Cotizando para: —

Cotizador Financiero

Cotizador Financiemiento Automotriz“ Cotizador general.”
La cotización es temporal y es estimada hasta hacer el análisis completo de la operación, Puede variar la tasa dependiendo el perfil crediticio.
La marca y la unidad mostrarán aquí su imagen si existe en el catálogo.
Datos generales y parámetros del crédito
Agencia
Vendedor
Tipo de póliza Descripción seguro Seguro 12 meses
Código postal Sexo Fecha nacimiento
Edad Año modelo
Nuevo usa año actual o año siguiente. Seminuevo cotiza sin valor factura.
Tipo de unidad
Marca
Modelo
Versión
Precio del vehículo
Selecciona marca, modelo y versión para intentar sugerir precio del catálogo interno.
Condición Nuevo: cotiza con valor factura solo para año actual o año siguiente.
Seguro WS
Banorte · ANA · Latino · Qualitas
Disponible cuando el tipo de póliza es Anual Contado.
Aseguradora Prima de seguro
Fecha cotización Fecha primer pago Plazo seleccionado 60 meses
Enganche %
Ej: 40 = 40%
Enganche $ (MXN)
Ej: 35000
Accesorios financiados 8% máx.
Tasa fija anual sin IVA
Ej: 14.99 = 14.99%
Comisión por apertura
Ej: 3 = 3%
GPS obligatorio
IVA usado en fórmulas 16%
Resultados intermedios
Enganche monto 0.00 Financiamiento vehículo 0.00 Seguro auto financiado 0.00
Seguro Vida y Desempleo 0.00 Monto a financiar 0.00 factor.vd (según plazo) 0.000000
Pago inicial (Enganche + comisión + GPS + seguro contado)
Comisión apertura c/IVA 0.00 Instalación GPS 0.00 Renta mensual GPS 0.00
Pago a seguro contado 0.00 Total pago a agencia 0.00 Total pago inicial 0.00
CAT y mensualidades
CAT anual 0.00% Mensualidad crédito (sin GPS) 0.00 Mensualidad total (con GPS) 0.00
Enganche (monto)
$0.00
Pago inicial total
$0.00
Monto a financiar
$0.00
CAT (referencial)
0.00 %
Comparativo de mensualidades por plazo
Calculado con los mismos datos capturados arriba. Haz click en un plazo para actualizar la tabla de amortización inferior.
Plazo Monto a financiar Pago inicial Mensualidad crédito Mensualidad total (año 1) Mensualidad (mes 13+) CAT aprox.
Los resultados son informativos y pueden variar por redondeos. Ajusta solo lo necesario para empatar con la financiera.
Tabla de amortización · 60 meses
Se genera con la misma lógica que la hoja: interés simple mensual sobre saldo, IVA sobre intereses, mensualidad fija del crédito + renta mensual de GPS.
No. Pago Fecha Saldo inicial Pago a capital Intereses IVA intereses Mensualidad total
Totales 0.00 0.00 0.00 0.00
`; } // --- NUEVO: calcular fecha de primer pago basada en fechaCotizacion ------ function calcularFechaPrimerPagoDesdeCotizacion() { const MIN_DIAS = 30; const fcInput = document.getElementById("fechaCotizacion"); const fpInput = document.getElementById("fechaPrimerPago"); if (!fcInput || !fpInput) return; // Si viene "hoy" o vacío, lo sustituimos por la fecha actual (YYYY-MM-DD) let fcStr = fcInput.value; const hoy = new Date(); const yyyy = hoy.getFullYear(); const mm = String(hoy.getMonth() + 1).padStart(2, "0"); const dd = String(hoy.getDate()).padStart(2, "0"); if (!fcStr || fcStr === "hoy" || fcStr.length !== 10) { fcStr = `${yyyy}-${mm}-${dd}`; fcInput.value = fcStr; } const partes = fcStr.split("-"); if (partes.length !== 3) return; let anio = parseInt(partes[0], 10); let mes = parseInt(partes[1], 10) - 1; // 0-based let dia = parseInt(partes[2], 10); let d = new Date(anio, mes, dia); d.setDate(d.getDate() + MIN_DIAS); // suma 30 días anio = d.getFullYear(); mes = d.getMonth(); dia = d.getDate(); // Ajuste a día 1 o 16 hacia adelante let diaPago; if (dia <= 1) { diaPago = 1; } else if (dia <= 16) { diaPago = 16; } else { mes += 1; if (mes > 11) { mes = 0; anio += 1; } diaPago = 1; } const fechaPago = new Date(anio, mes, diaPago); const yyyy2 = fechaPago.getFullYear(); const mm2 = String(fechaPago.getMonth() + 1).padStart(2, "0"); const dd2 = String(fechaPago.getDate()).padStart(2, "0"); fpInput.value = `${yyyy2}-${mm2}-${dd2}`; } // --- Escenario de plazo usado para el comparativo ------------------------ function calcularEscenarioPlazo(plazoEsc, data) { const iva = 0.16; const precio = data.precio; const engPct = data.engPct; const tasaAnual = data.tasaAnual; const comAperturaPct = data.comAperturaPct; const accesoriosFin = data.accesoriosFin; const tipoPoliza = data.tipoPoliza; const primaSeguro = data.primaSeguro; const primaSeguroAnual = Number(data.primaSeguroAnual || 0) || 0; const usaGps = data.usaGps; if (plazoEsc <= 0 || precio <= 0) return null; // factor.vd (tabla Var!A2:B7) const factorTabla = { 12: 0.00318, 24: 0.00569, 36: 0.00812, 48: 0.01078, 60: 0.01370, 72: 0.01690 }; const factorVd = factorTabla[plazoEsc] || 0; const E11 = precio * engPct; const E12 = precio - E11; const E14 = (tipoPoliza === "Multianual Financiado") ? primaSeguro : 0; const baseSeguroVD = E12 + accesoriosFin + E14; const E15 = factorVd !== 0 ? (factorVd * baseSeguroVD) / (1 - factorVd) : 0; const E16 = E12 + accesoriosFin + E15 + E14; const C20 = E16 * comAperturaPct * (1 + iva); const E20 = usaGps ? 1500 * (1 + iva) : 0; const E21 = usaGps ? 250 * (1 + iva) : 0; const C22 = (tipoPoliza !== "Multianual Financiado") ? primaSeguro : 0; const C23 = E11 + C20 + E20 + 0 + C22; const rateMensualPMT = tasaAnual * (1 + iva) / 12; const mensualCredito = (plazoEsc > 0 && E16 > 0) ? pmt(rateMensualPMT, plazoEsc, E16) : 0; const seguroAnualMensual = (tipoPoliza === "Multianual Financiado" && primaSeguroAnual > 0) ? (primaSeguroAnual / 12) : 0; const mensualTotalYear1 = mensualCredito + E21 + seguroAnualMensual; const mensualTotalPost12 = mensualCredito + E21; // Flujo para IRR (CAT) const flujoIRR = []; const flujoInicial = -(E12 + accesoriosFin - C20 / (1 + iva)); flujoIRR.push(flujoInicial); let saldo = E16; for (let k = 1; k <= plazoEsc; k++) { const interes = saldo * tasaAnual / 12; const ivaInteres = interes * iva; let capital = mensualCredito - interes - ivaInteres; if (k === plazoEsc) { capital = saldo; } const pagoCreditoConIVA = capital + interes + ivaInteres; const segAnualMes = (tipoPoliza === "Multianual Financiado" && k <= 12) ? seguroAnualMensual : 0; const pagoTotalMes = pagoCreditoConIVA + E21 + segAnualMes; const flujoMes = pagoTotalMes - ivaInteres; flujoIRR.push(flujoMes); saldo -= capital; if (saldo < 0) saldo = 0; } let CAT = 0; if (flujoIRR.length > 1 && E16 > 0 && plazoEsc > 0) { const irrMensual = irr(flujoIRR, 0.02); if (isFinite(irrMensual)) { CAT = Math.pow(1 + irrMensual, 12) - 1; } } return { plazo: plazoEsc, engancheMonto: E11, montoFinanciar: E16, pagoInicial: C23, mensualCredito: mensualCredito, mensualTotalYear1: mensualTotalYear1, mensualTotalPost12: mensualTotalPost12, CAT: CAT }; } function actualizarComparativo(data) { const tbody = document.getElementById("tbodyComparativo"); if (!tbody) return; tbody.innerHTML = ""; const plazosComparar = [12, 24, 36, 48, 60, 72]; const plazoActual = getCurrentPlazoValue(Number(data?.plazoActual || _comparativoPlazoActivo || 60) || 60); setCurrentPlazoValue(plazoActual); plazosComparar.forEach(plazoEsc => { const r = calcularEscenarioPlazo(plazoEsc, data); if (!r) return; const tr = document.createElement("tr"); tr.className = "is-clickable"; tr.setAttribute('data-plazo', String(plazoEsc)); if (plazoEsc === _comparativoPlazoActivo) tr.classList.add("is-active"); const tdPlazo = document.createElement("td"); const plazoBtn = document.createElement("button"); plazoBtn.type = "button"; plazoBtn.className = "comparativo-plazo-btn"; plazoBtn.textContent = plazoEsc + " meses"; plazoBtn.setAttribute('data-select-plazo', String(plazoEsc)); plazoBtn.setAttribute('aria-label', `Usar plazo de ${plazoEsc} meses`); plazoBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); selectComparativoPlazo(plazoEsc); }); tdPlazo.appendChild(plazoBtn); tr.appendChild(tdPlazo); const tdMonto = document.createElement("td"); tdMonto.textContent = formatMoney(r.montoFinanciar); tr.appendChild(tdMonto); const tdPagoInicial = document.createElement("td"); tdPagoInicial.textContent = formatMoney(r.pagoInicial); tr.appendChild(tdPagoInicial); const tdMenCred = document.createElement("td"); tdMenCred.textContent = formatMoney(r.mensualCredito); tr.appendChild(tdMenCred); const tdMenTot = document.createElement("td"); tdMenTot.textContent = formatMoney(r.mensualTotalYear1); tr.appendChild(tdMenTot); const tdMenPost = document.createElement("td"); tdMenPost.textContent = (data.tipoPoliza === "Multianual Financiado" && plazoEsc > 12) ? formatMoney(r.mensualTotalPost12) : "-"; tr.appendChild(tdMenPost); const tdCAT = document.createElement("td"); tdCAT.className = "cat-cell"; tdCAT.textContent = formatPercent(r.CAT); tr.appendChild(tdCAT); tr.addEventListener('click', () => selectComparativoPlazo(plazoEsc)); tbody.appendChild(tr); }); highlightComparativoPlazo(plazoActual); } // --- Lógica principal del cotizador (réplica del Excel) ------------------- function calcularCotizacion() { const iva = 0.16; const precio = toNumber("precioVehiculo"); // E9 const engPctInput = toNumber("enganchePct"); // E10 (% o decimal legacy) const engMontoInput = toNumber("engancheMonto"); // ($) const plazo = setCurrentPlazoValue(getCurrentPlazoValue(60)); // C13 const tasaAnualPct = toNumber("tasaAnual"); // C14 (% o decimal legacy) const comAperturaPctInput = toNumber("comApertura"); // C15 (% o decimal legacy) const accesoriosFin = toNumber("accesoriosFin"); // E13 const tipoPoliza = document.getElementById("tipoPoliza").value; // G5 const primaSeguro = toNumber("primaSeguro"); // G6 const primaSeguroAnual = toNumber("primaSeguroAnual"); // (solo Multianual) const usaGps = document.getElementById("usaGps").value === "si"; // Normalizaciones para fórmulas (sin mover fórmulas del Excel): // - engPct / tasaAnual / comAperturaPct deben ser decimales (0.4 = 40%) const { engPct01 } = syncEngancheInputs({ precio, engPctInput, engMontoInput, }); const engPct = engPct01; const tasaAnual = pctInputTo01(tasaAnualPct); const comAperturaPct = pctInputTo01(comAperturaPctInput); // factor.vd (tabla Var!A2:B7) const factorTabla = { 12: 0.00318, 24: 0.00569, 36: 0.00812, 48: 0.01078, 60: 0.01370, 72: 0.01690 }; const factorVd = factorTabla[plazo] || 0; // Descripción del seguro let descSeguro; if (tipoPoliza === "Multianual Financiado") { descSeguro = "Seguro " + plazo + " meses"; } else { descSeguro = "Seguro 12 meses"; } document.getElementById("descSeguro").textContent = descSeguro; // E11 = E9*E10 const E11 = precio * engPct; // E12 = E9 - E11 const E12 = precio - E11; // E14 const E14 = (tipoPoliza === "Multianual Financiado") ? primaSeguro : 0; // E15 const baseSeguroVD = E12 + accesoriosFin + E14; const E15 = factorVd !== 0 ? (factorVd * baseSeguroVD) / (1 - factorVd) : 0; // E16 const E16 = E12 + accesoriosFin + E15 + E14; // C20 = E16 * C15 * 1.16 const C20 = E16 * comAperturaPct * (1 + iva); // E20 / E21 const E20 = usaGps ? 1500 * (1 + iva) : 0; const E21 = usaGps ? 250 * (1 + iva) : 0; // C22 const C22 = (tipoPoliza !== "Multianual Financiado") ? primaSeguro : 0; // C23 const C23 = E11 + C20 + E20 + 0 + C22; // Mensualidad crédito const rateMensualPMT = tasaAnual * (1 + iva) / 12; const mensualCredito = (plazo > 0 && E16 > 0) ? pmt(rateMensualPMT, plazo, E16) : 0; const mensualTotal = mensualCredito + E21; const seguroAnualMensual = (tipoPoliza === "Multianual Financiado" && primaSeguroAnual > 0) ? (primaSeguroAnual / 12) : 0; const mensualTotalYear1 = mensualTotal + seguroAnualMensual; const mensualTotalPost12 = mensualTotal; // --- Tabla de amortización y flujo para IRR ---------------------------- const tbody = document.getElementById("tbodyAmort"); tbody.innerHTML = ""; let saldo = E16; const flujoIRR = []; const flujoInicial = -(E12 + accesoriosFin - C20 / (1 + iva)); flujoIRR.push(flujoInicial); const pagosCapital = []; const pagosInteres = []; const pagosIVA = []; const pagosTotales = []; let baseDate = null; const fpInput = document.getElementById("fechaPrimerPago").value; if (fpInput) { const d = new Date(fpInput); if (!isNaN(d.getTime())) baseDate = d; } for (let k = 1; k <= plazo; k++) { const interes = saldo * tasaAnual / 12; const ivaInteres = interes * iva; let capital = mensualCredito - interes - ivaInteres; if (k === plazo) { capital = saldo; } const pagoCreditoConIVA = capital + interes + ivaInteres; const segAnualMes = (tipoPoliza === "Multianual Financiado" && k <= 12) ? seguroAnualMensual : 0; const pagoTotalMes = pagoCreditoConIVA + E21 + segAnualMes; const flujoMes = pagoTotalMes - ivaInteres; flujoIRR.push(flujoMes); pagosCapital.push(capital); pagosInteres.push(interes); pagosIVA.push(ivaInteres); pagosTotales.push(pagoTotalMes); const tr = document.createElement("tr"); const tdN = document.createElement("td"); tdN.textContent = k; tr.appendChild(tdN); const tdF = document.createElement("td"); if (baseDate) { const fecha = addMonths(baseDate, k - 1); tdF.textContent = formatDate(fecha); } else { tdF.textContent = ""; } tr.appendChild(tdF); const tdS = document.createElement("td"); tdS.textContent = formatMoney(saldo); tr.appendChild(tdS); const tdCap = document.createElement("td"); tdCap.textContent = formatMoney(capital); tr.appendChild(tdCap); const tdInt = document.createElement("td"); tdInt.textContent = formatMoney(interes); tr.appendChild(tdInt); const tdIVA = document.createElement("td"); tdIVA.textContent = formatMoney(ivaInteres); tr.appendChild(tdIVA); const tdTot = document.createElement("td"); tdTot.textContent = formatMoney(pagoTotalMes); tr.appendChild(tdTot); tbody.appendChild(tr); saldo -= capital; if (saldo < 0) saldo = 0; } document.getElementById("totCapital").textContent = formatMoney(sumar(pagosCapital)); document.getElementById("totInteres").textContent = formatMoney(sumar(pagosInteres)); document.getElementById("totIVA").textContent = formatMoney(sumar(pagosIVA)); document.getElementById("totPagos").textContent = formatMoney(sumar(pagosTotales)); let CAT = 0; if (flujoIRR.length > 1 && E16 > 0 && plazo > 0) { const irrMensual = irr(flujoIRR, 0.02); if (isFinite(irrMensual)) { CAT = Math.pow(1 + irrMensual, 12) - 1; } } // --- Pintar resultados en la "hoja" ------------------------------------ document.getElementById("outE11").textContent = formatMoney(E11); document.getElementById("outE12").textContent = formatMoney(E12); document.getElementById("outE14").textContent = formatMoney(E14); document.getElementById("outE15").textContent = formatMoney(E15); document.getElementById("outE16").textContent = formatMoney(E16); document.getElementById("outFactorVd").textContent = factorVd.toFixed(6); document.getElementById("outC20").textContent = formatMoney(C20); document.getElementById("outE20").textContent = formatMoney(E20); document.getElementById("outE21").textContent = formatMoney(E21); document.getElementById("outC22").textContent = formatMoney(C22); document.getElementById("outE22").textContent = formatMoney(E11 + C20 + E20 + 0); document.getElementById("outC23").textContent = formatMoney(C23); document.getElementById("outCAT").textContent = formatPercent(CAT); document.getElementById("outMensualCredito").textContent = formatMoney(mensualCredito); document.getElementById("outMensualTotal").textContent = formatMoney(tipoPoliza === "Multianual Financiado" ? mensualTotalYear1 : mensualTotal); // UI extra para Multianual: prima anual + mensualidad post 12 const showMulti = tipoPoliza === "Multianual Financiado"; const rowPrimaAnual = document.getElementById("row-prima-anual-multi"); const rowMultiExtraEl = document.getElementById("row-multi-extra"); if (rowPrimaAnual) rowPrimaAnual.style.display = showMulti ? "" : "none"; if (rowMultiExtraEl) rowMultiExtraEl.style.display = showMulti ? "" : "none"; const outSegEl = document.getElementById("outSeguroAnualMensual"); if (outSegEl) outSegEl.textContent = showMulti ? formatMoney(seguroAnualMensual) : "$0.00"; const outPostEl = document.getElementById("outMensualTotalPost12"); if (outPostEl) outPostEl.textContent = showMulti ? formatMoney(mensualTotalPost12) : "$0.00"; document.getElementById("sumEnganche").textContent = formatMoney(E11); document.getElementById("sumPagoInicial").textContent = formatMoney(C23); document.getElementById("sumMontoFinanciar").textContent = formatMoney(E16); document.getElementById("sumCAT").textContent = formatPercent(CAT); const amortPlazo = document.getElementById("amortPlazoActual"); if (amortPlazo) amortPlazo.textContent = `· ${plazo} meses`; const marca = String(document.getElementById("marcaVehiculoText")?.value || document.getElementById("marcaVehiculoInput")?.value || "").trim(); const version = String(document.getElementById("versionVehiculoText")?.value || document.getElementById("versionVehiculoInput")?.value || "").trim(); const pill = document.getElementById("pillResumen"); pill.textContent = `${marca} ${version} · ${plazo} meses · CAT aprox. ${(CAT * 100).toFixed(2)}%`; // --- Actualizar comparativo con los mismos datos ----------------------- const dataCommon = { precio, engPct, tasaAnual, comAperturaPct, accesoriosFin, tipoPoliza, primaSeguro, primaSeguroAnual, usaGps, plazoActual: plazo }; actualizarComparativo(dataCommon); } // Sincronizar Enganche $ <-> % según el último campo editado (function wireEngancheSync() { const pctEl = document.getElementById('enganchePct'); const monEl = document.getElementById('engancheMonto'); const precioEl = document.getElementById('precioVehiculo'); if (pctEl) { pctEl.addEventListener('input', () => { _engancheLastInput = 'pct'; try { syncEngancheInputs({ precio: toNumber('precioVehiculo'), engPctInput: toNumber('enganchePct'), engMontoInput: toNumber('engancheMonto'), source: 'pct' }); } catch (_) {} }); } if (monEl) { monEl.addEventListener('input', () => { _engancheLastInput = 'monto'; try { syncEngancheInputs({ precio: toNumber('precioVehiculo'), engPctInput: toNumber('enganchePct'), engMontoInput: toNumber('engancheMonto'), source: 'monto' }); } catch (_) {} }); } if (precioEl) { precioEl.addEventListener('input', () => { try { syncEngancheInputs({ precio: toNumber('precioVehiculo'), engPctInput: toNumber('enganchePct'), engMontoInput: toNumber('engancheMonto') }); } catch (_) {} }); } })(); function toggleMultianualUI() { const tipo = document.getElementById('tipoPoliza')?.value || ''; const show = tipo === 'Multianual Financiado'; const rowPrima = document.getElementById('row-prima-anual-multi'); const rowExtra = document.getElementById('row-multi-extra'); if (rowPrima) rowPrima.style.display = show ? '' : 'none'; if (rowExtra) rowExtra.style.display = show ? '' : 'none'; } function syncPolicyDescription() { const desc = document.getElementById('descSeguro'); if (!desc) return; if (_seguroWsState.selectedIndex >= 0) return; const tipo = String(document.getElementById('tipoPoliza')?.value || '').trim(); desc.textContent = tipo === 'Multianual Financiado' ? 'Seguro multianual financiado' : tipo === 'Multianual Pago Anual' ? 'Seguro multianual pago anual' : 'Seguro 12 meses'; } (function wireTipoPoliza() { const sel = document.getElementById('tipoPoliza'); const inpAnual = document.getElementById('primaSeguroAnual'); if (sel) { sel.addEventListener('change', () => { toggleMultianualUI(); clearSeguroWsState(); syncPolicyDescription(); updateSeguroWsUi(); scheduleCotizacionRecalc(true); }); } if (inpAnual) { inpAnual.addEventListener('input', () => { scheduleCotizacionRecalc(); }); } toggleMultianualUI(); syncPolicyDescription(); })(); function wireDynamicCotizacionInputs() { const immediateIds = ['precioVehiculo', 'enganchePct', 'engancheMonto', 'tasaAnual', 'comApertura', 'accesoriosFin', 'primaSeguro', 'primaSeguroAnual', 'fechaPrimerPago']; immediateIds.forEach((id) => { const el = document.getElementById(id); if (!el) return; ['input', 'change'].forEach((evtName) => { el.addEventListener(evtName, () => scheduleCotizacionRecalc()); }); }); const gps = document.getElementById('usaGps'); gps?.addEventListener('change', () => scheduleCotizacionRecalc(true)); } function limpiarResultados() { _precioManual = false; _anioManual = false; setCurrentPlazoValue(60); const idsText = [ "outE11","outE12","outE14","outE15","outE16","outFactorVd", "outC20","outE20","outE21","outC22","outE22","outC23", "outCAT","outMensualCredito","outMensualTotal","outSeguroAnualMensual","outMensualTotalPost12", "sumEnganche","sumPagoInicial","sumMontoFinanciar","sumCAT", "pillResumen","totCapital","totInteres","totIVA","totPagos" ]; idsText.forEach(id => { const el = document.getElementById(id); if (!el) return; if (id === "outCAT" || id === "sumCAT") { el.textContent = "0.00 %"; } else if (id === "pillResumen") { el.textContent = ""; } else if (id === "totCapital" || id === "totInteres" || id === "totIVA" || id === "totPagos") { el.textContent = "$0.00"; } else { el.textContent = "$0.00"; } }); document.getElementById("tbodyAmort").innerHTML = ""; document.getElementById("tbodyComparativo").innerHTML = ""; const amortPlazo = document.getElementById("amortPlazoActual"); if (amortPlazo) amortPlazo.textContent = "· sin seleccionar"; } // --- Datos que vamos a mandar a la solicitud MStar ------------------------- function obtenerDatosCotizacionParaGuardar() { const data = { agencia: document.getElementById("nombreAgencia") ? document.getElementById("nombreAgencia").value.trim() : "", fechaCotizacion: document.getElementById("fechaCotizacion").value, fechaPrimerPago: document.getElementById("fechaPrimerPago").value, marca: (document.getElementById("marcaVehiculo").value || "").trim(), version: (document.getElementById("versionVehiculo").value || "").trim(), precioVehiculo: toNumber("precioVehiculo"), enganchePct: toNumber("enganchePct"), engancheMonto: toNumber("engancheMonto"), plazo: getCurrentPlazoValue(), // Tomamos los textos ya formateados que ve el usuario engancheMontoTxt: document.getElementById("outE11").textContent, montoFinanciarTxt: document.getElementById("outE16").textContent, pagoInicialTxt: document.getElementById("outC23").textContent, mensualidadCreditoTxt: document.getElementById("outMensualCredito").textContent, mensualidadTotalTxt: document.getElementById("outMensualTotal").textContent, catTxt: document.getElementById("outCAT").textContent }; return data; } async function guardarCotizacion() { // Aseguramos que la cotización esté fresca scheduleCotizacionRecalc(true); const data = obtenerDatosCotizacionParaGuardar(); if (isEmbedMode && !data.agencia) { alert("Por favor captura el nombre de la agencia (Nombre del Canal) antes de guardar."); return; } // Guardar en MongoDB try { const resp = await fetch("/api/mstar/cotizaciones", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); if (!resp.ok) { throw new Error("HTTP " + resp.status); } const result = await resp.json(); const cotizacionId = result._id || result.id; if (!cotizacionId) { throw new Error("No se recibió ID de cotización"); } // Redirige al formulario de solicitud con el ID en la URL window.location.href = "/admin/mstar-solicitud.html?cotizacion_id=" + encodeURIComponent(cotizacionId); } catch (e) { console.error("Error guardando cotización:", e); alert("⚠️ Error al guardar la cotización en el servidor.\n\n" + e.message); } } // --- Carga de entidades (agencias, vendedores) ---------------------------- let _entidades = []; async function cargarEntidades() { try { const resp = await fetch('/api/red/entidades'); if (!resp.ok) throw new Error('HTTP ' + resp.status); const result = await resp.json(); // El API devuelve {ok: true, data: {entidades: [...]}} if (result.ok && result.data && result.data.entidades) { _entidades = result.data.entidades; } else if (Array.isArray(result)) { _entidades = result; } else { _entidades = []; } console.log('Entidades cargadas:', _entidades.length); poblarAgencias(); poblarMarcas(); // Si viene embebido desde portal de cliente/campaña, forzar vendedor (AGS/Allan) try { const qs = new URLSearchParams(window.location.search || ''); const embed = qs.get('embed') === '1'; if (embed) { const qVendId = String(qs.get('vendedorId') || '').trim(); const qVendEmail = String(qs.get('vendedorEmail') || '').trim().toLowerCase(); let vend = null; if (qVendId) vend = _entidades.find((e) => e && e.tipo === 'vendedor' && String(e._id) === qVendId) || null; if (!vend && qVendEmail) { vend = _entidades.find((e) => e && e.tipo === 'vendedor' && String(e.email || '').trim().toLowerCase() === qVendEmail) || null; } const agenciaId = String(qs.get('agenciaId') || vend?.agenciaId || '').trim(); if (agenciaId) setAgenciaSelection(agenciaId, { preserveVendor: true }); if (vend && vend._id) setVendedorSelection(vend); } } catch (_) {} } catch (e) { console.error('Error cargando entidades:', e); } } function poblarAgencias() { const sel = document.getElementById('agenciaId'); if (!sel) return; const agencias = _entidades.filter(e => e.tipo === 'agencia'); sel.innerHTML = ''; agencias.sort((a, b) => (a.nombre || '').localeCompare(b.nombre || '')); agencias.forEach(a => { const opt = document.createElement('option'); opt.value = a._id; opt.textContent = a.nombre || 'Sin nombre'; sel.appendChild(opt); }); // En el cotizador publico no forzamos agencia por default. if (!isEmbedMode) { sel.value = ''; sel.selectedIndex = 0; const hidNombre = document.getElementById('nombreAgencia'); if (hidNombre) hidNombre.value = ''; } } function poblarVendedores(agenciaId) { const sel = document.getElementById('vendedorId'); if (!sel) return; sel.innerHTML = ''; if (!agenciaId) { sel.value = ''; sel.selectedIndex = 0; return; } const vendedores = _entidades.filter(e => e.tipo === 'vendedor' && e.agenciaId === agenciaId); vendedores.sort((a, b) => { const na = [a.nombre, a.apPaterno, a.apMaterno].filter(Boolean).join(' '); const nb = [b.nombre, b.apPaterno, b.apMaterno].filter(Boolean).join(' '); return na.localeCompare(nb); }); vendedores.forEach(v => { const opt = document.createElement('option'); opt.value = v._id; opt.textContent = [v.nombre, v.apPaterno, v.apMaterno].filter(Boolean).join(' ') || 'Sin nombre'; sel.appendChild(opt); }); if (!isEmbedMode) { sel.value = ''; sel.selectedIndex = 0; } } function agenciaDisplayName(agencia) { return String(agencia?.nombre || '').trim(); } function vendedorDisplayName(vendedor) { return [vendedor?.nombre, vendedor?.apPaterno, vendedor?.apMaterno].filter(Boolean).join(' ').trim(); } function syncVisibleAgenciaVendedorInputs() { const selAgencia = document.getElementById('agenciaId'); const selVend = document.getElementById('vendedorId'); const agenciaInput = document.getElementById('agenciaInput'); const vendedorInput = document.getElementById('vendedorInput'); const agencia = _entidades.find((e) => e && e.tipo === 'agencia' && String(e._id) === String(selAgencia?.value || '')) || null; const vendedor = _entidades.find((e) => e && e.tipo === 'vendedor' && String(e._id) === String(selVend?.value || '')) || null; if (agenciaInput) agenciaInput.value = agencia ? agenciaDisplayName(agencia) : ''; if (vendedorInput) vendedorInput.value = vendedor ? vendedorDisplayName(vendedor) : ''; const hidNombre = document.getElementById('nombreAgencia'); if (hidNombre) hidNombre.value = agencia ? agenciaDisplayName(agencia) : ''; } function setAgenciaSelection(agenciaRef, { preserveVendor = false } = {}) { const selAgencia = document.getElementById('agenciaId'); const agencia = (typeof agenciaRef === 'object' && agenciaRef) || _entidades.find((e) => e && e.tipo === 'agencia' && String(e._id) === String(agenciaRef || '')) || null; if (!selAgencia) return; selAgencia.value = agencia ? String(agencia._id || '') : ''; poblarVendedores(agencia ? String(agencia._id || '') : ''); if (!preserveVendor) { const selVend = document.getElementById('vendedorId'); if (selVend) selVend.value = ''; } syncVisibleAgenciaVendedorInputs(); } function setVendedorSelection(vendedorRef) { const selVend = document.getElementById('vendedorId'); const vendedor = (typeof vendedorRef === 'object' && vendedorRef) || _entidades.find((e) => e && e.tipo === 'vendedor' && String(e._id) === String(vendedorRef || '')) || null; if (!selVend) return; if (vendedor?.agenciaId) { setAgenciaSelection(vendedor.agenciaId, { preserveVendor: true }); poblarVendedores(String(vendedor.agenciaId || '')); } selVend.value = vendedor ? String(vendedor._id || '') : ''; syncVisibleAgenciaVendedorInputs(); } function openAgenciaSuggest(query) { const agencias = _entidades .filter((e) => e && e.tipo === 'agencia') .sort((a, b) => agenciaDisplayName(a).localeCompare(agenciaDisplayName(b), 'es', { sensitivity: 'base' })); const q = normalizeVehicleText(query); const items = agencias .filter((agencia) => !q || normalizeVehicleText(agenciaDisplayName(agencia)).includes(q)) .slice(0, 12) .map((agencia) => ({ title: agenciaDisplayName(agencia) || 'Agencia', meta: String(agencia?.ciudad || agencia?.estado || agencia?.email || '').trim(), pick: () => setAgenciaSelection(agencia), })); showSuggest(document.getElementById('agenciaSuggest'), items, (item) => item.pick(), 'No encontré agencias con ese texto.'); } function openVendedorSuggest(query) { const agenciaId = String(document.getElementById('agenciaId')?.value || '').trim(); const vendedores = _entidades .filter((e) => e && e.tipo === 'vendedor' && (!agenciaId || String(e.agenciaId || '') === agenciaId)) .sort((a, b) => vendedorDisplayName(a).localeCompare(vendedorDisplayName(b), 'es', { sensitivity: 'base' })); const q = normalizeVehicleText(query); const items = vendedores .filter((vend) => !q || normalizeVehicleText(`${vendedorDisplayName(vend)} ${vend?.email || ''}`).includes(q)) .slice(0, 14) .map((vend) => { const agencia = _entidades.find((e) => e && e.tipo === 'agencia' && String(e._id) === String(vend?.agenciaId || '')) || null; return { title: vendedorDisplayName(vend) || 'Vendedor', meta: [agenciaDisplayName(agencia), vend?.email].filter(Boolean).join(' · '), pick: () => setVendedorSelection(vend), }; }); showSuggest(document.getElementById('vendedorSuggest'), items, (item) => item.pick(), agenciaId ? 'No encontré vendedores en esa agencia.' : 'Primero selecciona o busca una agencia.'); } // --- Poblar Marcas, Modelos y Versiones --- let _marcaActual = null; // Guarda la marca seleccionada con sus modelos embebidos let _modeloActual = null; // Guarda el modelo seleccionado con sus versiones let _suggestCloseTimer = null; const CATALOG_API = '/api/red/catalogo-amis'; const _amisCatalogState = { brandsByYear: new Map(), modelsByBrandYear: new Map(), versionsByPairYear: new Map(), }; let _amisVehicleSelection = { brand: '', model: '', versionDoc: null, }; let _suspendPrecioManualDetection = false; function _keyName(s) { try { const v = String(s || '').trim().toLowerCase(); if (!v) return ''; return v .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') // sin acentos .replace(/[^a-z0-9]+/g, ''); } catch (_) { return String(s || '').trim().toLowerCase().replace(/\s+/g, ' '); } } function normalizeVehicleText(value) { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); } async function fetchAmisCatalog(path) { const res = await fetch(`${CATALOG_API}${path}`, { credentials: 'include' }); const json = await res.json().catch(() => ({})); if (!res.ok || json?.ok === false) throw new Error(json?.error || `HTTP ${res.status}`); return json; } function validCatalogYear(value) { return /^\d{4}$/.test(String(value || '').trim()); } async function ensureAmisBrands(year) { const yearKey = String(year || '').trim(); if (!validCatalogYear(yearKey)) return []; if (_amisCatalogState.brandsByYear.has(yearKey)) return _amisCatalogState.brandsByYear.get(yearKey); const seen = new Set(); const rows = []; for (let offset = 0; offset < 3000; offset += 500) { const json = await fetchAmisCatalog(`/buscar?categoria=AUTOS&anio=${encodeURIComponent(yearKey)}&limit=500&offset=${offset}`); const docs = Array.isArray(json.data) ? json.data : []; docs.forEach((doc) => { const brand = String(doc?.marca || '').trim(); if (brand) seen.add(brand); }); if (docs.length < 500) break; } rows.push(...Array.from(seen).sort((a, b) => String(a || '').localeCompare(String(b || ''), 'es', { sensitivity: 'base' }))); _amisCatalogState.brandsByYear.set(yearKey, rows); return rows; } async function searchAmisBrands(year, query) { const yearKey = String(year || '').trim(); const q = String(query || '').trim(); if (!validCatalogYear(yearKey)) return []; const allBrands = await ensureAmisBrands(yearKey); if (!q) return allBrands; const qNorm = normalizeVehicleText(q); const local = allBrands.filter((brand) => normalizeVehicleText(brand).includes(qNorm)); if (local.length) return local; const json = await fetchAmisCatalog(`/buscar?categoria=AUTOS&anio=${encodeURIComponent(yearKey)}&q=${encodeURIComponent(q)}&limit=200`); return [...new Set((Array.isArray(json.data) ? json.data : []).map((doc) => String(doc?.marca || '').trim()).filter(Boolean))] .sort((a, b) => String(a || '').localeCompare(String(b || ''), 'es', { sensitivity: 'base' })); } async function ensureAmisModels(brand, year) { const brandKey = String(brand || '').trim().toUpperCase(); const yearKey = String(year || '').trim(); const cacheKey = `${brandKey}::${yearKey}`; if (!brandKey || !validCatalogYear(yearKey)) return []; if (_amisCatalogState.modelsByBrandYear.has(cacheKey)) return _amisCatalogState.modelsByBrandYear.get(cacheKey); const json = await fetchAmisCatalog(`/buscar?categoria=AUTOS&marca=${encodeURIComponent(brandKey)}&anio=${encodeURIComponent(yearKey)}&limit=500`); const rows = [...new Set((Array.isArray(json.data) ? json.data : []).map((doc) => String(doc?.tipo || '').trim()).filter(Boolean))] .sort((a, b) => String(a || '').localeCompare(String(b || ''), 'es', { sensitivity: 'base' })); _amisCatalogState.modelsByBrandYear.set(cacheKey, rows); return rows; } async function ensureAmisVersions(brand, model, year) { const brandKey = String(brand || '').trim().toUpperCase(); const modelKey = String(model || '').trim().toUpperCase(); const yearKey = String(year || '').trim(); const cacheKey = `${brandKey}::${modelKey}::${yearKey}`; if (!brandKey || !modelKey || !validCatalogYear(yearKey)) return []; if (_amisCatalogState.versionsByPairYear.has(cacheKey)) return _amisCatalogState.versionsByPairYear.get(cacheKey); const json = await fetchAmisCatalog(`/buscar?categoria=AUTOS&marca=${encodeURIComponent(brandKey)}&tipo=${encodeURIComponent(modelKey)}&anio=${encodeURIComponent(yearKey)}&limit=500`); const rows = Array.isArray(json.data) ? json.data : []; _amisCatalogState.versionsByPairYear.set(cacheKey, rows); return rows; } function matchAndSliceVehicle(rows, query, mapper, max = 10) { const q = normalizeVehicleText(query); const prepared = (Array.isArray(rows) ? rows : []).map(mapper); if (!q) return prepared.slice(0, max); return prepared.filter((item) => normalizeVehicleText(`${item.title || ''} ${item.meta || ''}`).includes(q)).slice(0, max); } function setVehicleTexts({ marca = '', modelo = '', version = '' } = {}) { const marcaInput = document.getElementById('marcaVehiculoInput'); const modeloInput = document.getElementById('modeloVehiculoInput'); const versionInput = document.getElementById('versionVehiculoInput'); const hidMarcaText = document.getElementById('marcaVehiculoText'); const hidModeloText = document.getElementById('modeloVehiculoText'); const hidVersionText = document.getElementById('versionVehiculoText'); if (marcaInput && marca !== undefined) marcaInput.value = String(marca || '').trim(); if (modeloInput && modelo !== undefined) modeloInput.value = String(modelo || '').trim(); if (versionInput && version !== undefined) versionInput.value = String(version || '').trim(); if (hidMarcaText && marca !== undefined) hidMarcaText.value = String(marca || '').trim(); if (hidModeloText && modelo !== undefined) hidModeloText.value = String(modelo || '').trim(); if (hidVersionText && version !== undefined) hidVersionText.value = String(version || '').trim(); } function clearHiddenVehicleSelects({ marca = false, modelo = false, version = false } = {}) { const selMarca = document.getElementById('marcaVehiculo'); const selModelo = document.getElementById('modeloVehiculo'); const selVersion = document.getElementById('versionVehiculo'); if (marca && selMarca) selMarca.value = ''; if ((marca || modelo) && selModelo) selModelo.innerHTML = ''; if ((marca || modelo || version) && selVersion) selVersion.innerHTML = ''; } function extractAmisCatalogPrice(doc) { const candidateKeys = ['precio', 'valor', 'valor_comercial', 'valorComercial', 'importe']; for (const key of candidateKeys) { const raw = doc && typeof doc === 'object' ? doc[key] : null; const num = Number(raw || 0); if (Number.isFinite(num) && num > 0) return num; } return 0; } function formatCatalogMoney(value) { const n = Number(value || 0); if (!Number.isFinite(n) || n <= 0) return '$0.00'; return `$${n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function updateVehiclePriceHint({ state = 'idle', price = 0 } = {}) { const hint = document.getElementById('precioVehiculoHint'); if (!hint) return; if (state === 'found' && Number(price || 0) > 0) { hint.textContent = `Se encontró un precio en el catálogo: ${formatCatalogMoney(price)}. El precio puede variar.`; hint.style.color = '#86efac'; return; } if (state === 'missing') { hint.textContent = 'No se encontró. Favor de capturar el valor de la unidad.'; hint.style.color = '#fcd34d'; return; } hint.textContent = 'Selecciona marca, modelo y versión para intentar sugerir precio del catálogo.'; hint.style.color = ''; } function canonicalVehicleVersionText(value) { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toUpperCase() .replace(/\b1[.,]5\s*L\b/g, ' 15L ') .replace(/\b2[.,]0\s*L\b/g, ' 20L ') .replace(/\bSTD\b/g, ' MANUAL ') .replace(/\bSTANDARD\b/g, ' MANUAL ') .replace(/\bTM\d*\b/g, ' MANUAL ') .replace(/\bMT\d*\b/g, ' MANUAL ') .replace(/\bTA\d*\b/g, ' AUTO ') .replace(/\bAT\d*\b/g, ' AUTO ') .replace(/\bAUTOMATICA\b/g, ' AUTO ') .replace(/\bAUTOMATICO\b/g, ' AUTO ') .replace(/\bA\/A\b/g, ' AC ') .replace(/\bAA\b/g, ' AC ') .replace(/\b4P\b/g, ' ') .replace(/\b5P\b/g, ' ') .replace(/\bCOM\b/g, ' ') .replace(/[^A-Z0-9]+/g, ' ') .replace(/\s+/g, ' ') .trim(); } function tokenizeVehicleVersion(value) { const stop = new Set(['AC', 'L', 'TM', 'MT', 'AT', 'TA', 'AUTO', 'COM']); return canonicalVehicleVersionText(value) .split(/\s+/) .map((token) => String(token || '').trim()) .filter((token) => token && !stop.has(token) && token.length > 1); } function scoreVehicleVersionMatch(targetValue, candidateValue) { const targetNorm = canonicalVehicleVersionText(targetValue); const candidateNorm = canonicalVehicleVersionText(candidateValue); if (!targetNorm || !candidateNorm) return 0; if (targetNorm === candidateNorm) return 1000; if (candidateNorm.includes(targetNorm) || targetNorm.includes(candidateNorm)) return 700; const targetTokens = Array.from(new Set(tokenizeVehicleVersion(targetNorm))); const candidateTokens = new Set(tokenizeVehicleVersion(candidateNorm)); if (!targetTokens.length || !candidateTokens.size) return 0; let matched = 0; let score = 0; targetTokens.forEach((token) => { if (!candidateTokens.has(token)) return; matched += 1; score += token.length >= 5 ? 5 : 3; }); if (!matched) return 0; const coverage = matched / targetTokens.length; score += Math.round(coverage * 20); if (matched >= 2) score += 10; return score; } function deriveCatalogVersionHint(value) { const tokens = canonicalVehicleVersionText(value).split(/\s+/).filter(Boolean); if (!tokens.length) return ''; const hardStop = /^(?:\d+P|L\d|V\d|[12]\dL|ABS|AC|R\d+|OCUP|OCCUP|MANUAL|AUTO|STD|TM\d*|MT\d*|AT\d*|TA\d*|CVT?AUTO)$/; const out = []; for (const token of tokens) { if (hardStop.test(token)) break; out.push(token); if (out.length >= 3) break; } return out.join(' ').trim(); } function findInternalCatalogVersionData(marca, modelo, version) { const marcaKey = _keyName(marca || ''); const modeloKey = _keyName(modelo || ''); const versionKey = _keyName(version || ''); if (!marcaKey || !modeloKey) return null; const marcas = Array.isArray(_entidades) ? _entidades.filter((e) => e && e.tipo === 'marca') : []; const marcaDoc = marcas.find((m) => _keyName(m?.nombre || '') === marcaKey) || marcas.find((m) => { const mk = _keyName(m?.nombre || ''); return mk && (mk.includes(marcaKey) || marcaKey.includes(mk)); }) || null; if (!marcaDoc) return null; const modelos = Array.isArray(marcaDoc.modelos) ? marcaDoc.modelos : []; const modeloDoc = modelos.find((m) => _keyName(m?.nombre || '') === modeloKey) || modelos.find((m) => { const mk = _keyName(m?.nombre || ''); return mk && (mk.includes(modeloKey) || modeloKey.includes(mk)); }) || null; if (!modeloDoc) return null; const versiones = Array.isArray(modeloDoc.versiones) ? modeloDoc.versiones : []; if (!versionKey) return null; const versionDoc = versiones.find((v) => _keyName(v?.nombre || '') === versionKey) || versiones.find((v) => { const vk = _keyName(v?.nombre || ''); return vk && (vk.includes(versionKey) || versionKey.includes(vk)); }) || null; if (versionDoc) return versionDoc; let best = null; let bestScore = 0; let secondScore = 0; versiones.forEach((v) => { const score = scoreVehicleVersionMatch(version, String(v?.nombre || '').trim()); if (score > bestScore) { secondScore = bestScore; bestScore = score; best = v; } else if (score > secondScore) { secondScore = score; } }); if (!best) return null; if (bestScore < 22) return null; if (bestScore - secondScore < 4 && bestScore < 40) return null; return best; } function findInternalCatalogBrandData(marca) { const marcaKey = _keyName(marca || ''); if (!marcaKey) return null; const marcas = Array.isArray(_entidades) ? _entidades.filter((e) => e && e.tipo === 'marca') : []; return ( marcas.find((m) => _keyName(m?.nombre || '') === marcaKey) || marcas.find((m) => { const mk = _keyName(m?.nombre || ''); return mk && (mk.includes(marcaKey) || marcaKey.includes(mk)); }) || null ); } function findInternalCatalogModelData(marca, modelo) { const brandDoc = findInternalCatalogBrandData(marca); if (!brandDoc) return null; const modeloKey = _keyName(modelo || ''); const modelos = Array.isArray(brandDoc.modelos) ? brandDoc.modelos : []; return ( modelos.find((m) => _keyName(m?.nombre || '') === modeloKey) || modelos.find((m) => { const mk = _keyName(m?.nombre || ''); return mk && (mk.includes(modeloKey) || modeloKey.includes(mk)); }) || null ); } function pickCatalogPhotoUrl(row) { if (!row || typeof row !== 'object') return ''; const direct = String(row.photo || row.photoUrl || row.imageUrl || row.foto || '').trim(); if (direct) return direct; const photos = Array.isArray(row.fotos) ? row.fotos : Array.isArray(row.photos) ? row.photos : []; const first = photos.find((item) => String(item?.url || item?.imageUrl || '').trim()); return String(first?.url || first?.imageUrl || '').trim(); } function attachVehicleMediaFallbacks(host) { if (!host) return; Array.from(host.querySelectorAll('img[data-media-fallback]')).forEach((img) => { img.addEventListener('error', () => { const target = String(img.getAttribute('data-media-fallback') || '').trim(); const parent = img.closest(target || '.vehicle-media-card'); if (!parent) return; const message = target === '.vehicle-media-brand' ? 'Sin logo disponible' : 'Sin foto disponible para esta unidad.'; parent.innerHTML = `
${message}
`; }, { once: true }); }); } function renderVehicleMediaPanel({ marca = '', modelo = '', version = '', amisDoc = null } = {}) { const host = document.getElementById('vehicleMediaPanel'); if (!host) return; const brandDoc = findInternalCatalogBrandData(marca); const modelDoc = findInternalCatalogModelData(marca, modelo); const logoUrl = String(brandDoc?.logoUrl || brandDoc?.imageUrl || '').trim(); const photoUrl = pickCatalogPhotoUrl(modelDoc) || String(amisDoc?.photoUrl || amisDoc?.imageUrl || amisDoc?.foto || '').trim(); const title = [String(marca || '').trim(), String(modelo || '').trim(), String(version || '').trim()].filter(Boolean).join(' '); const subtitle = String(amisDoc?.descripcion || '').trim(); if (!logoUrl && !photoUrl && !title) { host.innerHTML = `
La marca y la unidad mostrarán aquí su imagen si existe en el catálogo.
`; return; } host.innerHTML = `
${logoUrl ? `${escapeSeguroWsHtml(marca || 'Marca')}` : `
Sin logo disponible
`}
${photoUrl ? `
${escapeSeguroWsHtml(title || 'Unidad')}
Catálogo
${escapeSeguroWsHtml(title || marca || modelo || 'Unidad seleccionada')}
${escapeSeguroWsHtml(subtitle || [marca, modelo].filter(Boolean).join(' · ') || 'Si el catálogo tiene foto o logo, se muestra aquí automáticamente.')}
` : `
Sin foto disponible para esta unidad.
`}
`; attachVehicleMediaFallbacks(host); } function refreshVehicleMediaPanel() { renderVehicleMediaPanel({ marca: String(document.getElementById('marcaVehiculoInput')?.value || document.getElementById('marcaVehiculoText')?.value || '').trim(), modelo: String(document.getElementById('modeloVehiculoInput')?.value || document.getElementById('modeloVehiculoText')?.value || '').trim(), version: String(document.getElementById('versionVehiculoInput')?.value || document.getElementById('versionVehiculoText')?.value || '').trim(), amisDoc: _amisVehicleSelection.versionDoc, }); } function applyAutoDetectedVehiclePrice({ marca = '', modelo = '', version = '', versionHint = '', amisDoc = null } = {}) { const inputPrecio = document.getElementById('precioVehiculo'); const canEvaluate = String(marca || '').trim() && String(modelo || '').trim() && String(version || '').trim(); renderVehicleMediaPanel({ marca, modelo, version, amisDoc }); if (!canEvaluate) { updateVehiclePriceHint({ state: 'idle' }); return; } if (!inputPrecio) return; const shortHint = String(versionHint || '').trim() || deriveCatalogVersionHint(version); const versionDoc = findInternalCatalogVersionData(marca, modelo, shortHint) || findInternalCatalogVersionData(marca, modelo, version); const internalPrice = Number(versionDoc?.precio || 0) || 0; const amisPrice = extractAmisCatalogPrice(amisDoc); const price = internalPrice > 0 ? internalPrice : amisPrice; if (!(price > 0)) { updateVehiclePriceHint({ state: 'missing' }); return; } updateVehiclePriceHint({ state: 'found', price }); if (_precioManual) return; _suspendPrecioManualDetection = true; inputPrecio.value = String(Math.round(price)); _suspendPrecioManualDetection = false; try { calcularCotizacion(); } catch (_) {} } function poblarMarcas() { const sel = document.getElementById('marcaVehiculo'); if (!sel) return; // Dedup por nombre (sin borrar nada): en el catálogo puede haber marcas duplicadas por _id. // Preferimos la marca que tenga modelos o la más reciente. const marcasAll = _entidades.filter(e => e.tipo === 'marca' && e.nombre); const byKey = new Map(); marcasAll.forEach((m) => { const name = String(m?.nombre || '').trim(); if (!name) return; const k = _keyName(name) || name.toLowerCase(); const hasModelos = Array.isArray(m?.modelos) && m.modelos.length ? 1 : 0; const ts = String(m?.updatedAt || m?.createdAt || ''); const prev = byKey.get(k); if (!prev) { byKey.set(k, { m, hasModelos, ts }); return; } if (hasModelos > (prev.hasModelos || 0)) { byKey.set(k, { m, hasModelos, ts }); return; } if (hasModelos < (prev.hasModelos || 0)) return; if (ts && (!prev.ts || ts > prev.ts)) byKey.set(k, { m, hasModelos, ts }); }); const marcas = Array.from(byKey.values()).map(x => x.m); sel.innerHTML = ''; marcas.sort((a, b) => (a.nombre || '').localeCompare(b.nombre || '')); marcas.forEach(m => { const opt = document.createElement('option'); opt.value = m._id; opt.textContent = m.nombre || 'Sin nombre'; sel.appendChild(opt); }); } function hideSuggest(node) { if (!node) return; node.classList.remove('show'); node.innerHTML = ''; } function hideAllVehicleSuggests() { hideSuggest(document.getElementById('agenciaSuggest')); hideSuggest(document.getElementById('vendedorSuggest')); hideSuggest(document.getElementById('marcaVehiculoSuggest')); hideSuggest(document.getElementById('modeloVehiculoSuggest')); hideSuggest(document.getElementById('versionVehiculoSuggest')); } function scheduleHideVehicleSuggests() { clearTimeout(_suggestCloseTimer); _suggestCloseTimer = setTimeout(() => hideAllVehicleSuggests(), 140); } function showSuggest(node, items, onPick, emptyText) { if (!node) return; if (!Array.isArray(items) || !items.length) { node.innerHTML = `
${emptyText || 'Sin coincidencias. Puedes seguir capturando manualmente.'}
`; node.classList.add('show'); return; } node.innerHTML = `
${items.map((item, index) => ` `).join('')}
`; node.classList.add('show'); Array.from(node.querySelectorAll('[data-pro-suggest-index]')).forEach((btn) => { btn.addEventListener('mousedown', (ev) => ev.preventDefault()); btn.addEventListener('click', () => { const idx = Number(btn.getAttribute('data-pro-suggest-index') || '-1'); if (idx < 0 || idx >= items.length) return; onPick(items[idx]); hideSuggest(node); }); }); } function syncVisibleVehicleInputs() { const marcaInput = document.getElementById('marcaVehiculoInput'); const modeloInput = document.getElementById('modeloVehiculoInput'); const versionInput = document.getElementById('versionVehiculoInput'); const hidMarcaText = document.getElementById('marcaVehiculoText'); const hidModeloText = document.getElementById('modeloVehiculoText'); const hidVersionText = document.getElementById('versionVehiculoText'); if (marcaInput) marcaInput.value = String(hidMarcaText?.value || marcaInput.value || '').trim(); if (modeloInput) modeloInput.value = String(hidModeloText?.value || modeloInput.value || '').trim(); if (versionInput) versionInput.value = String(hidVersionText?.value || versionInput.value || '').trim(); } function setMarcaSelection(brand) { const year = String(document.getElementById('anioModelo')?.value || '').trim(); const brandValue = String(brand || '').trim(); _amisVehicleSelection.brand = brandValue; _amisVehicleSelection.model = ''; _amisVehicleSelection.versionDoc = null; clearHiddenVehicleSelects({ marca: true, modelo: true, version: true }); setVehicleTexts({ marca: brandValue, modelo: '', version: '' }); refreshVehicleMediaPanel(); updateVehiclePriceHint({ state: 'idle' }); if (validCatalogYear(year) && brandValue) { ensureAmisModels(brandValue, year).catch(() => []); } clearSeguroWsState(); updateSeguroWsUi(); } function setModeloSelection(model) { const year = String(document.getElementById('anioModelo')?.value || '').trim(); const modelValue = String(model || '').trim(); _amisVehicleSelection.model = modelValue; _amisVehicleSelection.versionDoc = null; clearHiddenVehicleSelects({ modelo: true, version: true }); setVehicleTexts({ marca: _amisVehicleSelection.brand || document.getElementById('marcaVehiculoInput')?.value || '', modelo: modelValue, version: '', }); refreshVehicleMediaPanel(); updateVehiclePriceHint({ state: 'idle' }); if (validCatalogYear(year) && _amisVehicleSelection.brand && modelValue) { ensureAmisVersions(_amisVehicleSelection.brand, modelValue, year).catch(() => []); } clearSeguroWsState(); updateSeguroWsUi(); } function setVersionSelection(versionDoc) { const doc = versionDoc && typeof versionDoc === 'object' ? versionDoc : null; const versionHint = String(document.getElementById('versionVehiculoInput')?.value || '').trim(); const versionValue = String(doc?.descripcion || doc?.version || '').trim(); _amisVehicleSelection.versionDoc = doc; clearHiddenVehicleSelects({ version: true }); setVehicleTexts({ marca: _amisVehicleSelection.brand || document.getElementById('marcaVehiculoInput')?.value || '', modelo: _amisVehicleSelection.model || document.getElementById('modeloVehiculoInput')?.value || '', version: versionValue, }); refreshVehicleMediaPanel(); applyAutoDetectedVehiclePrice({ marca: _amisVehicleSelection.brand || '', modelo: _amisVehicleSelection.model || '', version: versionValue, versionHint, amisDoc: doc, }); clearSeguroWsState(); updateSeguroWsUi(); } function poblarModelos(marcaId) { const sel = document.getElementById('modeloVehiculo'); const selVersion = document.getElementById('versionVehiculo'); if (!sel) return; sel.innerHTML = ''; if (selVersion) selVersion.innerHTML = ''; _marcaActual = null; _modeloActual = null; if (!marcaId) return; // Buscar la marca y sus modelos embebidos const marca = _entidades.find(e => e._id === marcaId && e.tipo === 'marca'); if (!marca) return; _marcaActual = marca; const modelos = Array.isArray(marca.modelos) ? marca.modelos : []; modelos.sort((a, b) => (a.nombre || '').localeCompare(b.nombre || '')); modelos.forEach((m, idx) => { const opt = document.createElement('option'); opt.value = idx; // Usamos el índice como valor opt.textContent = m.nombre || 'Sin nombre'; sel.appendChild(opt); }); } function poblarVersiones(modeloIdx) { const sel = document.getElementById('versionVehiculo'); if (!sel) return; sel.innerHTML = ''; _modeloActual = null; if (modeloIdx === '' || modeloIdx === null || modeloIdx === undefined) return; if (!_marcaActual || !Array.isArray(_marcaActual.modelos)) return; const modelo = _marcaActual.modelos[parseInt(modeloIdx, 10)]; if (!modelo) return; _modeloActual = modelo; const versiones = Array.isArray(modelo.versiones) ? modelo.versiones : []; versiones.forEach((v, idx) => { const opt = document.createElement('option'); opt.value = idx; const precioStr = v.precio ? ` - $${Number(v.precio).toLocaleString()}` : ''; opt.textContent = (v.nombre || 'Sin nombre') + precioStr; opt.dataset.precio = v.precio || 0; sel.appendChild(opt); }); } function initMarcaModeloUI() { const selMarca = document.getElementById('marcaVehiculo'); const selModelo = document.getElementById('modeloVehiculo'); const selVersion = document.getElementById('versionVehiculo'); const inpMarca = document.getElementById('marcaVehiculoInput'); const inpModelo = document.getElementById('modeloVehiculoInput'); const inpVersion = document.getElementById('versionVehiculoInput'); const hidMarcaText = document.getElementById('marcaVehiculoText'); const hidModeloText = document.getElementById('modeloVehiculoText'); const hidVersionText = document.getElementById('versionVehiculoText'); const inputPrecio = document.getElementById('precioVehiculo'); const inputTipoUnidad = document.getElementById('tipoUnidadSeguro'); if (selMarca) { selMarca.addEventListener('change', () => { const marcaId = selMarca.value; resetVehicleCatalogPriceLock(); poblarModelos(marcaId); // Guardar nombre de marca en hidden const marca = _entidades.find(e => e._id === marcaId); if (hidMarcaText) hidMarcaText.value = marca?.nombre || ''; // Limpiar modelo y versión if (hidModeloText) hidModeloText.value = ''; if (hidVersionText) hidVersionText.value = ''; if (inpModelo) inpModelo.value = ''; if (inpVersion) inpVersion.value = ''; syncVisibleVehicleInputs(); refreshVehicleMediaPanel(); clearSeguroWsState(); updateSeguroWsUi(); }); } if (selModelo) { selModelo.addEventListener('change', () => { const modeloIdx = selModelo.value; resetVehicleCatalogPriceLock(); poblarVersiones(modeloIdx); // Guardar nombre de modelo en hidden if (_marcaActual && _marcaActual.modelos && modeloIdx !== '') { const modelo = _marcaActual.modelos[parseInt(modeloIdx, 10)]; if (hidModeloText) hidModeloText.value = modelo?.nombre || ''; } else { if (hidModeloText) hidModeloText.value = ''; } // Limpiar versión if (hidVersionText) hidVersionText.value = ''; if (inpVersion) inpVersion.value = ''; syncVisibleVehicleInputs(); refreshVehicleMediaPanel(); clearSeguroWsState(); updateSeguroWsUi(); }); } if (selVersion) { selVersion.addEventListener('change', () => { const versionIdx = selVersion.value; resetVehicleCatalogPriceLock(); if (_modeloActual && _modeloActual.versiones && versionIdx !== '') { const version = _modeloActual.versiones[parseInt(versionIdx, 10)]; if (hidVersionText) hidVersionText.value = version?.nombre || ''; // Auto-llenar precio solo si NO fue editado manualmente if (version?.precio && inputPrecio && !_precioManual) { _suspendPrecioManualDetection = true; inputPrecio.value = version.precio; _suspendPrecioManualDetection = false; try { calcularCotizacion(); } catch (_) {} } } else { if (hidVersionText) hidVersionText.value = ''; } syncVisibleVehicleInputs(); refreshVehicleMediaPanel(); clearSeguroWsState(); updateSeguroWsUi(); }); } // Detectar edición manual de precio y año modelo para fijarlos if (inputPrecio) { inputPrecio.addEventListener('input', () => { if (_suspendPrecioManualDetection) return; _precioManual = true; }); inputPrecio.addEventListener('keydown', () => { _precioManual = true; }); } const inputAnio = document.getElementById('anioModelo'); if (inputAnio) { inputAnio.addEventListener('input', () => { _anioManual = true; if (!_tipoUnidadManual) syncTipoUnidadSelection({ forceInfer: true }); else syncTipoUnidadUi(); _amisVehicleSelection = { brand: '', model: '', versionDoc: null }; setVehicleTexts({ marca: '', modelo: '', version: '' }); refreshVehicleMediaPanel(); clearHiddenVehicleSelects({ marca: true, modelo: true, version: true }); updateVehiclePriceHint({ state: 'idle' }); clearSeguroWsState(); updateSeguroWsUi(); }); inputAnio.addEventListener('keydown', () => { _anioManual = true; }); } const handleTipoUnidadChange = () => { _tipoUnidadManual = true; const tipo = getSelectedTipoUnidad(); if (tipo === 'nuevo' && inputAnio) { const allowedYears = getSeguroWsAllowedNewYears(); const anioActual = Math.floor(Number(inputAnio.value || 0)) || 0; if (!allowedYears.includes(anioActual)) { inputAnio.value = String(allowedYears[0]); inputAnio.dispatchEvent(new Event('input', { bubbles: true })); return; } } syncTipoUnidadUi(); clearSeguroWsState(); updateSeguroWsUi(); }; if (inputTipoUnidad) { inputTipoUnidad.addEventListener('change', handleTipoUnidadChange); } Array.from(document.querySelectorAll('[data-tipo-unidad]')).forEach((btnTipoUnidad) => { btnTipoUnidad.addEventListener('click', () => { const value = String(btnTipoUnidad.getAttribute('data-tipo-unidad') || '').trim().toLowerCase(); if (!inputTipoUnidad || !value) return; if (String(inputTipoUnidad.value || '').trim().toLowerCase() === value) { syncTipoUnidadUi(); return; } inputTipoUnidad.value = value; inputTipoUnidad.dispatchEvent(new Event('change', { bubbles: true })); }); }); const handleMarcaSuggest = async () => { if (!inpMarca) return; clearTimeout(_suggestCloseTimer); const year = String(inputAnio?.value || '').trim(); if (!validCatalogYear(year)) { showSuggest(document.getElementById('marcaVehiculoSuggest'), [], () => {}, 'Primero define el año modelo para filtrar marcas válidas en AMIS.'); return; } const brands = await searchAmisBrands(year, inpMarca.value).catch(() => []); const items = matchAndSliceVehicle(brands, inpMarca.value, (brand) => ({ title: brand, meta: `Marca disponible en ${year} dentro del catálogo AMIS`, pick: () => setMarcaSelection(brand), })); showSuggest( document.getElementById('marcaVehiculoSuggest'), items, (item) => item.pick(), 'No encontré marcas AMIS con ese texto. Puedes capturarla manualmente.' ); }; const handleModeloSuggest = async () => { if (!inpModelo) return; clearTimeout(_suggestCloseTimer); const year = String(inputAnio?.value || '').trim(); const brand = String(inpMarca?.value || _amisVehicleSelection.brand || '').trim(); if (!validCatalogYear(year)) { showSuggest(document.getElementById('modeloVehiculoSuggest'), [], () => {}, 'Primero define el año modelo para filtrar submarcas reales.'); return; } if (!brand) { showSuggest(document.getElementById('modeloVehiculoSuggest'), [], () => {}, 'Primero define la marca.'); return; } const modelos = await ensureAmisModels(brand, year).catch(() => []); const items = matchAndSliceVehicle(modelos, inpModelo.value, (model) => ({ title: model, meta: `Submarca / línea de ${brand.toUpperCase()} para ${year}`, pick: () => setModeloSelection(model), })); showSuggest( document.getElementById('modeloVehiculoSuggest'), items, (item) => item.pick(), 'No encontré submarcas/modelos AMIS con ese texto. Puedes capturarlo manualmente.' ); }; const handleVersionSuggest = async () => { if (!inpVersion) return; clearTimeout(_suggestCloseTimer); const year = String(inputAnio?.value || '').trim(); const brand = String(inpMarca?.value || _amisVehicleSelection.brand || '').trim(); const model = String(inpModelo?.value || _amisVehicleSelection.model || '').trim(); if (!validCatalogYear(year)) { showSuggest(document.getElementById('versionVehiculoSuggest'), [], () => {}, 'Primero define el año modelo para sugerir versiones válidas.'); return; } if (!brand || !model) { showSuggest(document.getElementById('versionVehiculoSuggest'), [], () => {}, 'Primero define marca y modelo.'); return; } const versiones = await ensureAmisVersions(brand, model, year).catch(() => []); const items = matchAndSliceVehicle(versiones, inpVersion.value, (doc) => { const label = String(doc?.descripcion || `${doc?.tipo || model} ${doc?.anio || ''}`).trim(); const catalogPrice = findInternalCatalogVersionData(brand, model, label)?.precio || extractAmisCatalogPrice(doc); return { title: label, meta: `${doc?.marca || brand} · ${doc?.tipo || model} · ${doc?.anio || year}${doc?.clave_amis ? ` · Clave ${doc.clave_amis}` : ''}${catalogPrice ? ` · Precio $${Number(catalogPrice).toLocaleString('es-MX')}` : ''}`, pick: () => setVersionSelection(doc), }; }, 10); showSuggest( document.getElementById('versionVehiculoSuggest'), items, (item) => item.pick(), 'No encontré versiones AMIS con ese texto. Puedes capturarla manualmente.' ); }; if (inpMarca) { inpMarca.addEventListener('focus', () => { handleMarcaSuggest().catch(() => {}); }); inpMarca.addEventListener('click', () => { handleMarcaSuggest().catch(() => {}); }); inpMarca.addEventListener('input', () => { resetVehicleCatalogPriceLock(); _amisVehicleSelection = { brand: inpMarca.value.trim(), model: '', versionDoc: null }; clearHiddenVehicleSelects({ marca: true, modelo: true, version: true }); setVehicleTexts({ marca: inpMarca.value.trim(), modelo: '', version: '' }); refreshVehicleMediaPanel(); updateVehiclePriceHint({ state: 'idle' }); clearSeguroWsState(); updateSeguroWsUi(); handleMarcaSuggest().catch(() => {}); }); inpMarca.addEventListener('blur', scheduleHideVehicleSuggests); } if (inpModelo) { inpModelo.addEventListener('focus', () => { handleModeloSuggest().catch(() => {}); }); inpModelo.addEventListener('click', () => { handleModeloSuggest().catch(() => {}); }); inpModelo.addEventListener('input', () => { resetVehicleCatalogPriceLock(); _amisVehicleSelection.model = inpModelo.value.trim(); _amisVehicleSelection.versionDoc = null; clearHiddenVehicleSelects({ modelo: true, version: true }); setVehicleTexts({ marca: inpMarca?.value || '', modelo: inpModelo.value.trim(), version: '', }); refreshVehicleMediaPanel(); updateVehiclePriceHint({ state: 'idle' }); clearSeguroWsState(); updateSeguroWsUi(); handleModeloSuggest().catch(() => {}); }); inpModelo.addEventListener('blur', scheduleHideVehicleSuggests); } if (inpVersion) { inpVersion.addEventListener('focus', () => { handleVersionSuggest().catch(() => {}); }); inpVersion.addEventListener('click', () => { handleVersionSuggest().catch(() => {}); }); inpVersion.addEventListener('input', () => { resetVehicleCatalogPriceLock(); _amisVehicleSelection.versionDoc = null; clearHiddenVehicleSelects({ version: true }); setVehicleTexts({ marca: inpMarca?.value || '', modelo: inpModelo?.value || '', version: inpVersion.value.trim(), }); refreshVehicleMediaPanel(); applyAutoDetectedVehiclePrice({ marca: inpMarca?.value || '', modelo: inpModelo?.value || '', version: inpVersion.value.trim(), amisDoc: null, }); clearSeguroWsState(); updateSeguroWsUi(); handleVersionSuggest().catch(() => {}); }); inpVersion.addEventListener('blur', scheduleHideVehicleSuggests); } } function clearSeguroWsState() { _seguroWsState = { loading: false, compare: null, selectedIndex: -1, publicUrl: '', warnings: [], insurerErrors: {} }; closeSeguroWsModal(); syncPolicyDescription(); } function closeSeguroWsModal() { _seguroWsModalOpen = false; const modal = document.getElementById('seguroWsModal'); if (modal) modal.hidden = true; } function renderSeguroWsModal() { const modal = document.getElementById('seguroWsModal'); const body = document.getElementById('seguroWsModalBody'); if (!modal || !body) return; const rows = Array.isArray(_seguroWsState.compare?.quotes) ? _seguroWsState.compare.quotes : []; const alerts = buildSeguroWsAlertsHtml(); body.innerHTML = ` ${alerts} ${rows.length ? rows.map((row, idx) => { const insurer = prettyInsurerName(row?.insurer?.nombre || ''); const total = Number(row?.quote?.TotalPrimaAnual || 0) || 0; const ref = String(row?.quote?.NumeroReferencia || row?.quote?.IDCotizacionWCF || '').trim(); const active = idx === _seguroWsState.selectedIndex; const suspicious = isSuspiciousSeguroWsPremium(total); return `
${escapeSeguroWsHtml(insurer)} ${suspicious ? 'Revisar tarifa' : ''}
${formatMoney(total)}${ref ? ` · ${escapeSeguroWsHtml(ref)}` : ''}
${suspicious ? '
La prima luce alta contra el valor capturado del vehículo. Conviene validar versión y código AMIS.
' : ''}
`; }).join('') : '
No hay opciones disponibles para mostrar.
'} `; Array.from(body.querySelectorAll('[data-seguro-ws-modal-apply]')).forEach((btnApply) => { btnApply.addEventListener('click', () => { const idx = Number(btnApply.getAttribute('data-seguro-ws-modal-apply') || -1); applySeguroWsSelection(idx); closeSeguroWsModal(); }); }); Array.from(body.querySelectorAll('[data-seguro-modal-close]')).forEach((btnClose) => { btnClose.addEventListener('click', closeSeguroWsModal); }); bindSeguroRetryButtons(body); } function openSeguroWsModal() { const rows = Array.isArray(_seguroWsState.compare?.quotes) ? _seguroWsState.compare.quotes : []; if (!rows.length) return; const modal = document.getElementById('seguroWsModal'); if (!modal) return; _seguroWsModalOpen = true; modal.hidden = false; renderSeguroWsModal(); } function escapeSeguroWsHtml(value) { return String(value == null ? '' : value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function normalizeInsurerKey(value) { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .replace(/[^a-z0-9]+/g, ''); } function prettyInsurerName(value) { const raw = String(value || '').trim(); const key = normalizeInsurerKey(raw); if (key.includes('banorte')) return 'Banorte'; if (key.includes('qualitas')) return 'Qualitas'; if (key.includes('latino')) return 'La Latinoamericana Seguros, S.A.'; if (key === 'ana' || key.includes('anaseguros')) return 'ANA'; return raw || 'Aseguradora'; } function getVehiclePriceForSanity() { return Number(document.getElementById('precioVehiculo')?.value || 0) || 0; } function isSuspiciousSeguroWsPremium(total) { const vehiclePrice = getVehiclePriceForSanity(); if (!(vehiclePrice > 0) || !(total > 0)) return false; return total / vehiclePrice >= 0.18; } function getSeguroWsErrorEntries() { return Object.entries(_seguroWsState.insurerErrors || {}).map(([key, message]) => ({ key, label: key === 'general' ? 'Servicio' : prettyInsurerName(key), message: String(message || '').trim() })).filter((entry) => entry.message); } function buildSeguroWsAlertsHtml() { const warnings = (Array.isArray(_seguroWsState.warnings) ? _seguroWsState.warnings : []) .map((msg) => `
Aviso de cotización
${escapeSeguroWsHtml(msg)}
`) .join(''); const errors = getSeguroWsErrorEntries() .map((entry) => { const insurerKey = normalizeInsurerKey(entry.key || entry.label || ''); const retryButton = insurerKey && insurerKey !== 'general' ? `` : ''; return `
${escapeSeguroWsHtml(entry.label)}
${retryButton}
${escapeSeguroWsHtml(entry.message)}
`; }) .join(''); return `${warnings}${errors}`; } function bindSeguroRetryButtons(scope) { if (!scope) return; Array.from(scope.querySelectorAll('[data-seguro-ws-retry]')).forEach((btnRetry) => { btnRetry.addEventListener('click', () => { const insurerKey = String(btnRetry.getAttribute('data-seguro-ws-retry') || '').trim(); closeSeguroWsModal(); cotizarSeguroWs(insurerKey).catch(() => {}); }); }); } function isSeguroWsAnnual() { return String(document.getElementById('tipoPoliza')?.value || '').trim().toLowerCase() === 'anual contado'; } function getSeguroWsAllowedNewYears() { const currentYear = new Date().getFullYear(); return [currentYear, currentYear + 1]; } function inferTipoUnidadFromAnio(anio) { const year = Number(anio || 0) || 0; if (!year) return 'seminuevo'; return year >= new Date().getFullYear() ? 'nuevo' : 'seminuevo'; } function getSelectedTipoUnidad() { const raw = String(document.getElementById('tipoUnidadSeguro')?.value || '').trim().toLowerCase(); if (raw === 'nuevo' || raw === 'seminuevo') return raw; return inferTipoUnidadFromAnio(Math.floor(toNumber('anioModelo')) || 0); } function syncTipoUnidadButtons() { const current = getSelectedTipoUnidad(); Array.from(document.querySelectorAll('[data-tipo-unidad]')).forEach((btn) => { const value = String(btn.getAttribute('data-tipo-unidad') || '').trim().toLowerCase(); const active = value === current; btn.classList.toggle('is-active', active); btn.setAttribute('aria-pressed', active ? 'true' : 'false'); }); } function syncTipoUnidadUi() { const hint = document.getElementById('tipoUnidadHint'); const summary = document.getElementById('tipoUnidadSummary'); const inputAnio = document.getElementById('anioModelo'); const tipo = getSelectedTipoUnidad(); const anio = Math.floor(toNumber('anioModelo')) || 0; const allowedYears = getSeguroWsAllowedNewYears(); const allowedText = `${allowedYears[0]} o ${allowedYears[1]}`; const invalidNewYear = tipo === 'nuevo' && anio > 0 && !allowedYears.includes(anio); if (inputAnio) { if (tipo === 'nuevo') { inputAnio.min = String(allowedYears[0]); inputAnio.max = String(allowedYears[1]); } else { inputAnio.removeAttribute('min'); inputAnio.removeAttribute('max'); } } if (hint) { hint.textContent = invalidNewYear ? `Nuevo solo permite ${allowedText}. Ajusta el año modelo para cotizar a valor factura.` : tipo === 'nuevo' ? `Nuevo usa ${allowedText} y cotiza con valor factura.` : 'Seminuevo cotiza sin valor factura y permite años anteriores.'; hint.style.color = invalidNewYear ? '#fca5a5' : ''; } if (summary) { summary.textContent = tipo === 'nuevo' ? `Nuevo: cotiza con valor factura solo para ${allowedText}.` : 'Seminuevo: cotiza sin valor factura y no fuerza precio a valor factura.'; } syncTipoUnidadButtons(); } function syncTipoUnidadSelection({ forceInfer = false } = {}) { const sel = document.getElementById('tipoUnidadSeguro'); if (!sel) return; if (forceInfer || !_tipoUnidadManual || !String(sel.value || '').trim()) { sel.value = inferTipoUnidadFromAnio(Math.floor(toNumber('anioModelo')) || 0); } syncTipoUnidadUi(); } function getVehicleDraftForSeguroWs() { const profile = getSeguroWsProfile(); const nombreCompleto = String(_clienteContext?.nombre || '').trim(); const nombreParts = nombreCompleto.split(/\s+/).filter(Boolean); const versionDoc = _amisVehicleSelection.versionDoc && typeof _amisVehicleSelection.versionDoc === 'object' ? _amisVehicleSelection.versionDoc : null; const marca = String(document.getElementById('marcaVehiculoInput')?.value || document.getElementById('marcaVehiculoText')?.value || '').trim(); const modelo = String(document.getElementById('modeloVehiculoInput')?.value || document.getElementById('modeloVehiculoText')?.value || '').trim(); const version = String(document.getElementById('versionVehiculoInput')?.value || document.getElementById('versionVehiculoText')?.value || '').trim(); const anio = Math.floor(toNumber('anioModelo')) || 0; const tipoUnidad = getSelectedTipoUnidad(); const precioVehiculo = Number(document.getElementById('precioVehiculo')?.value || 0) || 0; const valorFactura = tipoUnidad === 'nuevo' && precioVehiculo > 0 ? precioVehiculo : 0; return { tipoUnidad, tipo_unidad: tipoUnidad, nombre: String(_clienteContext?.nombres || nombreParts[0] || '').trim(), nombres: String(_clienteContext?.nombres || nombreParts[0] || '').trim(), apellidoPaterno: String(_clienteContext?.apellidoPaterno || _clienteContext?.apPaterno || nombreParts[1] || '').trim(), apellidoMaterno: String(_clienteContext?.apellidoMaterno || _clienteContext?.apMaterno || nombreParts.slice(2).join(' ') || '').trim(), email: String(_clienteContext?.email || _clienteContext?.mail || '').trim(), celular: String(_clienteContext?.celular || _clienteContext?.cel || '').trim(), fechaNacimiento: String(profile.fechaNacimiento || '').trim(), fecha_nacimiento: String(profile.fechaNacimiento || '').trim(), edad: Number(profile.edad || 0) || '', sexo: String(profile.sexo || '').trim(), marca, modelo, submarca: modelo, version, descripcion: String(versionDoc?.descripcion || version || '').trim(), anio, cp: profile.cp, codigo_postal: profile.cp, clave_amis: String(versionDoc?.clave_amis || '').trim(), amis: String(versionDoc?.clave_amis || '').trim(), valorFactura: valorFactura || '', fechaFactura: valorFactura > 0 ? todayFacturaDate() : '', }; } function applySeguroWsSelection(idx) { const rows = Array.isArray(_seguroWsState.compare?.quotes) ? _seguroWsState.compare.quotes : []; const row = idx >= 0 && idx < rows.length ? rows[idx] : null; if (!row) return; _seguroWsState.selectedIndex = idx; const total = Number(row?.quote?.TotalPrimaAnual || 0) || 0; const insurer = String(row?.insurer?.nombre || '').trim(); const primaInput = document.getElementById('primaSeguro'); const productoInput = document.getElementById('producto'); const descSeguro = document.getElementById('descSeguro'); if (primaInput) primaInput.value = total ? String(total.toFixed(2)) : '0'; if (productoInput && insurer) productoInput.value = insurer; if (descSeguro) descSeguro.textContent = insurer ? `Seguro 12 meses · ${insurer}` : 'Seguro 12 meses'; calcularCotizacion(); updateSeguroWsUi(); } function updateSeguroWsUi() { const btn = document.getElementById('btnCotizarSeguroWs'); const result = document.getElementById('seguroWsResult'); const hint = document.getElementById('seguroWsProfileHint'); const rows = Array.isArray(_seguroWsState.compare?.quotes) ? _seguroWsState.compare.quotes : []; const profile = getSeguroWsProfile(); const tipoUnidad = getSelectedTipoUnidad(); if (hint) { const bits = []; bits.push(`Unidad: ${tipoUnidad === 'nuevo' ? 'Nuevo' : 'Seminuevo'}`); if (profile.cp) bits.push(`CP: ${profile.cp}`); if (profile.sexo) bits.push(`Sexo: ${profile.sexo}`); if (Number(profile.edad || 0) > 0) bits.push(`Edad: ${profile.edad}`); if (profile.fechaNacimiento) bits.push(`Nacimiento: ${profile.fechaNacimiento}`); if (_amisVehicleSelection?.versionDoc?.clave_amis) bits.push(`AMIS: ${_amisVehicleSelection.versionDoc.clave_amis}`); hint.textContent = bits.length ? bits.join(' · ') : 'Banorte · ANA · Latino · Qualitas'; } if (!btn || !result) return; const annual = isSeguroWsAnnual(); btn.disabled = _seguroWsState.loading || !annual; btn.classList.toggle('is-loading', !!_seguroWsState.loading); btn.textContent = _seguroWsState.loading ? 'Cotizando…' : 'Cotizar seguro'; if (!annual) { const tipo = String(document.getElementById('tipoPoliza')?.value || '').trim(); result.innerHTML = `
${ tipo === 'Multianual Financiado' ? 'Multianual Financiado depende de la prima oficial de MStar. Captura la prima total y la prima anual del primer año.' : 'Multianual Pago Anual depende de la prima oficial de MStar. Captura manualmente la prima anual.' }
`; return; } if (_seguroWsState.loading) { result.innerHTML = `
Cotizando seguros con nuestros web services
Consultando Banorte, ANA, Latino y Qualitas. Esto puede tardar algunos segundos mientras validamos vehículo, CP y perfil del cliente.
Procesando
Banorte
ANA
Latino
Qualitas
`; return; } const alerts = buildSeguroWsAlertsHtml(); if (!rows.length) { const tipo = String(document.getElementById('tipoPoliza')?.value || '').trim(); const manualNote = tipo === 'Multianual Financiado' ? 'Captura la prima oficial multianual de MStar y la prima anual del primer año.' : tipo === 'Multianual Pago Anual' ? 'Captura la prima oficial anual de MStar para este esquema.' : 'Usa el botón para consultar opciones de seguro y aplicar la prima seleccionada.'; result.innerHTML = ` ${alerts}
${manualNote}
`; bindSeguroRetryButtons(result); return; } const selectedRow = _seguroWsState.selectedIndex >= 0 ? rows[_seguroWsState.selectedIndex] : null; const selectedInsurer = prettyInsurerName(selectedRow?.insurer?.nombre || ''); const selectedTotal = Number(selectedRow?.quote?.TotalPrimaAnual || 0) || 0; const suspicious = isSuspiciousSeguroWsPremium(selectedTotal); result.innerHTML = ` ${alerts}
${selectedRow ? `${escapeSeguroWsHtml(selectedInsurer)} seleccionada` : `${rows.length} opciones disponibles`} ${selectedRow && suspicious ? 'Prima alta' : ''}
${selectedRow ? `${formatMoney(selectedTotal)} fijada en Prima de seguro.` : 'Revisa el modal y elige la prima que quieras usar en el cálculo.'}
${selectedRow && suspicious ? '
Se ve alta frente al valor del vehículo. Conviene validar el AMIS antes de cerrar la cotización.
' : ''}
${_seguroWsState.publicUrl ? `
Comparativo: ${_seguroWsState.publicUrl}
` : ''} `; document.getElementById('btnSeguroWsOpenModal')?.addEventListener('click', openSeguroWsModal); bindSeguroRetryButtons(result); if (_seguroWsModalOpen) renderSeguroWsModal(); } async function cotizarSeguroWs(preferredInsurer = '') { if (!isSeguroWsAnnual()) { updateSeguroWsUi(); return; } const vehicle = getVehicleDraftForSeguroWs(); if (!vehicle.marca || !vehicle.modelo) { alert('Necesitas al menos Marca y Modelo para cotizar seguro.'); return; } if (!(vehicle.anio > 0)) { alert('Necesitas un año modelo válido para cotizar seguro.'); return; } if (vehicle.tipoUnidad === 'nuevo') { const allowedYears = getSeguroWsAllowedNewYears(); if (!allowedYears.includes(Number(vehicle.anio || 0))) { alert(`Para Nuevo solo puedes cotizar con año modelo ${allowedYears[0]} o ${allowedYears[1]}.`); return; } } if (String(vehicle.cp || '').length !== 5) { alert('Necesitas un código postal válido de 5 dígitos para cotizar seguro.'); return; } if (!String(vehicle.sexo || '').trim()) { alert('Necesitas capturar el sexo del cliente para cotizar seguro.'); return; } if (!(Number(vehicle.edad || 0) > 0) && !String(vehicle.fechaNacimiento || '').trim()) { alert('Necesitas Edad o Fecha de nacimiento para cotizar seguro.'); return; } _seguroWsState.loading = true; updateSeguroWsUi(); try { const resp = await fetch('/api/red/public/multicotizador/auto', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...vehicle, retryInsurer: preferredInsurer || undefined, }), }); const raw = await resp.text(); let json = {}; try { json = raw ? JSON.parse(raw) : {}; } catch (_) { json = {}; } if (!resp.ok || json?.ok === false) { const endpointError = resp.status === 404 ? 'La ruta de cotización no está publicada en este entorno (404).' : ''; const serverError = String(json?.error || json?.message || '').trim(); throw new Error(endpointError || serverError || `HTTP ${resp.status}`); } const compare = json?.data?.compare && typeof json.data.compare === 'object' ? json.data.compare : null; const rows = Array.isArray(compare?.quotes) ? compare.quotes.slice() : []; rows.sort((a, b) => (Number(a?.quote?.TotalPrimaAnual || 0) || 0) - (Number(b?.quote?.TotalPrimaAnual || 0) || 0)); _seguroWsState.compare = compare ? { ...compare, quotes: rows } : null; _seguroWsState.selectedIndex = -1; _seguroWsState.publicUrl = String(json?.data?.publicUrl || compare?.url || '').trim(); _seguroWsState.warnings = Array.isArray(json?.data?.warnings) ? json.data.warnings : []; _seguroWsState.insurerErrors = json?.data?.insurerErrors && typeof json.data.insurerErrors === 'object' ? json.data.insurerErrors : {}; if (rows.length) openSeguroWsModal(); } catch (e) { const rawMessage = String(e?.message || 'No pude cotizar seguro.').trim(); const message = /404/.test(rawMessage) ? 'La ruta de cotización no está publicada en este entorno (404).' : rawMessage; _seguroWsState.compare = null; _seguroWsState.selectedIndex = -1; _seguroWsState.publicUrl = ''; _seguroWsState.warnings = []; _seguroWsState.insurerErrors = { general: message }; } finally { _seguroWsState.loading = false; updateSeguroWsUi(); } } function initAgenciaVendedorUI() { const selAgencia = document.getElementById('agenciaId'); const selVend = document.getElementById('vendedorId'); const agenciaInput = document.getElementById('agenciaInput'); const vendedorInput = document.getElementById('vendedorInput'); if (selAgencia) { selAgencia.addEventListener('change', () => { setAgenciaSelection(selAgencia.value, { preserveVendor: false }); }); } if (selVend) { selVend.addEventListener('change', () => { syncVisibleAgenciaVendedorInputs(); }); } if (agenciaInput) { agenciaInput.addEventListener('focus', () => openAgenciaSuggest(agenciaInput.value)); agenciaInput.addEventListener('click', () => openAgenciaSuggest(agenciaInput.value)); agenciaInput.addEventListener('input', () => { if (!String(agenciaInput.value || '').trim()) { setAgenciaSelection('', { preserveVendor: false }); } openAgenciaSuggest(agenciaInput.value); }); agenciaInput.addEventListener('blur', scheduleHideVehicleSuggests); } if (vendedorInput) { vendedorInput.addEventListener('focus', () => openVendedorSuggest(vendedorInput.value)); vendedorInput.addEventListener('click', () => openVendedorSuggest(vendedorInput.value)); vendedorInput.addEventListener('input', () => { if (!String(vendedorInput.value || '').trim()) { if (selVend) selVend.value = ''; syncVisibleAgenciaVendedorInputs(); } openVendedorSuggest(vendedorInput.value); }); vendedorInput.addEventListener('blur', scheduleHideVehicleSuggests); } // Cargar entidades al inicio (siempre, para marcas/modelos) return cargarEntidades(); } // --- Inicialización ------------------------------------------------------ window.addEventListener("DOMContentLoaded", () => { // Ajustar fechaCotizacion (si está en "hoy") y fechaPrimerPago (30 días, solo 1 o 16) calcularFechaPrimerPagoDesdeCotizacion(); // Calcular cotización inicial con los valores por defecto calcularCotizacion(); // Cuando cambie la fecha de cotización, recalculamos primer pago y todo const fcInput = document.getElementById("fechaCotizacion"); if (fcInput) { fcInput.addEventListener("change", () => { calcularFechaPrimerPagoDesdeCotizacion(); calcularCotizacion(); }); } // Inicializar selectores de agencia/vendedor y marca/modelo const entityLoad = initAgenciaVendedorUI(); initMarcaModeloUI(); initComparativoPlazosUI(); const anioModeloInput = document.getElementById('anioModelo'); if (anioModeloInput && !String(anioModeloInput.value || '').trim()) { anioModeloInput.value = String(new Date().getFullYear()); } setCurrentPlazoValue(60); syncTipoUnidadSelection({ forceInfer: true }); syncComisionAperturaMode(); wireDynamicCotizacionInputs(); Promise.resolve(entityLoad).catch(() => {}).finally(() => loadPortalQuoteContext().catch(() => {})); document.getElementById('btnCotizarSeguroWs')?.addEventListener('click', () => { cotizarSeguroWs().catch(() => {}); }); document.getElementById('btnShareQuoteSend')?.addEventListener('click', () => { sendShareQuote().catch(() => {}); }); Array.from(document.querySelectorAll('[data-share-quote-close]')).forEach((btnClose) => { btnClose.addEventListener('click', closeShareQuoteModal); }); Array.from(document.querySelectorAll('[data-seguro-modal-close]')).forEach((btnClose) => { btnClose.addEventListener('click', closeSeguroWsModal); }); document.getElementById('seguroWsCp')?.addEventListener('input', () => { const cpEl = document.getElementById('seguroWsCp'); if (cpEl) cpEl.value = String(cpEl.value || '').replace(/\D+/g, '').slice(0, 5); clearSeguroWsState(); updateSeguroWsUi(); }); const birthField = document.getElementById('seguroWsFechaNacimiento'); ['input', 'change', 'blur'].forEach((evtName) => birthField?.addEventListener(evtName, () => { syncAgeFromBirthInput(); clearSeguroWsState(); updateSeguroWsUi(); })); document.getElementById('seguroWsEdad')?.addEventListener('input', () => { const ageEl = document.getElementById('seguroWsEdad'); if (ageEl) ageEl.value = String(ageEl.value || '').replace(/[^\d]/g, '').slice(0, 2); clearSeguroWsState(); updateSeguroWsUi(); }); document.getElementById('seguroWsSexo')?.addEventListener('change', () => { clearSeguroWsState(); updateSeguroWsUi(); }); document.getElementById('seguroWsModal')?.addEventListener('click', (event) => { if (event.target && event.target.matches('[data-seguro-modal-close]')) { closeSeguroWsModal(); } }); document.getElementById('shareQuoteModal')?.addEventListener('click', (event) => { if (event.target && event.target.matches('[data-share-quote-close]')) { closeShareQuoteModal(); } }); window.addEventListener('keydown', (event) => { if (event.key === 'Escape' && _seguroWsModalOpen) closeSeguroWsModal(); if (event.key === 'Escape' && _shareQuoteModalOpen) closeShareQuoteModal(); }); window.addEventListener('afterprint', cleanupPrintReport); fillSeguroWsProfileInputs({ cp: _clienteContext?.cp, fechaNacimiento: _clienteContext?.fechaNacimiento, edad: _clienteContext?.edad, sexo: _clienteContext?.sexo, }); syncAgeFromBirthInput(); syncVisibleAgenciaVendedorInputs(); refreshVehicleMediaPanel(); updateSeguroWsUi(); });