/* Perfil Hipotecario Inteligente™ — asistente conversacional + captura de lead */ const { useState: useStateC, useMemo: useMemoC, useEffect: useEffectC, useRef: useRefC } = React; const CCV = window.CCV; const num = (v) => { const n = parseFloat(String(v).replace(/[^0-9.-]/g, "")); return isNaN(n) ? 0 : n; }; // Pasos const S = { CONTACTO: 0, TIPO: 1, INGRESO: 2, ENGANCHE: 3, FIJOS: 4, TARJETAS: 5, URG: 6, DOLOR: 7, COMP: 8, DEC: 9, ACC: 10, RESULT: 11 }; const TOTAL = 12; const MSG = [ "Empecemos por lo básico", "Personalizando tu cálculo…", "Con tu ingreso ya estimamos tu capacidad base", "Tu ahorro fortalece tu perfil", "Calculando tu relación deuda / ingreso", "Afinando tu tope de endeudamiento", "Calculando tu plazo recomendado", "Personalizando tu propuesta", "Ya casi tenemos tu perfil…", "Ajustando los últimos detalles", "Generando tu Perfil Hipotecario…", "", ]; const TIPOS = [ { key: "nomina", ic: "👔", t: "Nómina", d: "Recibo mi sueldo por nómina" }, { key: "pfae", ic: "🧾", t: "Persona Física con Actividad Empresarial", d: "Facturo con alta en el SAT" }, { key: "empresario", ic: "🏭", t: "Empresario / Accionista", d: "Soy accionista de una empresa" }, { key: "honorarios", ic: "✍️", t: "Honorarios", d: "Cobro por honorarios profesionales" }, ]; // El cliente elige una actividad del catálogo maestro; cada banco aplica su propia tabla. const QI = { urg: { q: "¿Para cuándo te gustaría mudarte?", sub: "Nos ayuda a recomendarte el mejor plazo.", opts: [["Lo antes posible", 25], ["En los próximos 3 meses", 20], ["Este año", 13], ["Solo estoy explorando", 5]] }, dolor: { q: "¿Qué te motivó a buscar casa?", sub: "Para personalizar tu propuesta.", opts: [["Pago renta y quiero algo mío", 20], ["Mi familia necesita más espacio", 18], ["Quiero invertir y crear patrimonio", 14], ["Solo tengo curiosidad", 6]] }, comp: { q: "¿Ya encontraste una casa que te guste?", sub: "Así ajustamos tu simulación.", opts: [["Sí, ya tengo una en mente", 20], ["Estoy viendo varias", 13], ["Apenas voy a comenzar", 6]] }, dec: { q: "Cuando llegue el momento, ¿cómo tomarán la decisión?", sub: "Para acompañarte mejor en el proceso.", opts: [["Yo decido", 15], ["Mi pareja y yo", 12], ["Participa otro familiar", 8], ["Aún no lo hemos hablado", 4]] }, acc: { q: "¿Qué tan listo estás para iniciar tu trámite?", sub: "Último paso para armar tu perfil.", opts: [["Listo para empezar hoy", 20], ["En cuanto vea números que me convengan", 15], ["Necesito ordenar mis finanzas primero", 9], ["Solo estoy investigando", 4]] }, }; function MonthGrid({ values, setValues }) { return (
{values.map((v, i) => ( ))}
); } function ScoredQ({ q, onPick }) { return ( <>
{q.q}
{q.sub &&
{q.sub}
} {q.opts.map(([label, score]) => ( ))} ); } function CapacityFlow({ prop, enganchePct, anios, onClose, onSaved }) { const [step, setStep] = useStateC(0); const [type, setType] = useStateC(null); const [contacto, setContacto] = useStateC({ nombre: "", correo: "", celular: "" }); const [touched, setTouched] = useStateC(false); const [sueldo, setSueldo] = useStateC(""); const [m6, setM6] = useStateC(["", "", "", "", "", ""]); const [actividad, setActividad] = useStateC("servicios"); const [shares, setShares] = useStateC(""); const [nominaBank, setNominaBank] = useStateC(""); const [bonos, setBonos] = useStateC(""); const [clienteBancos, setClienteBancos] = useStateC([]); const [ahorro, setAhorro] = useStateC(""); const [fijos, setFijos] = useStateC({ hipoteca: "", auto: "", nomina: "", personales: "" }); const [tarjPago, setTarjPago] = useStateC(""); const [intent, setIntent] = useStateC({}); const [verDesglose, setVerDesglose] = useStateC(false); const savedRef = useRefC(false); const engineType = type === "honorarios" ? "pfae" : type; const engPct = num(ahorro) > 0 ? Math.max(0, Math.min(0.9, num(ahorro) / prop.precio)) : enganchePct; const celOK = /^\d{10}$/.test(contacto.celular.replace(/\D/g, "")); const correoOK = /\S+@\S+\.\S+/.test(contacto.correo); const contactoOK = contacto.nombre.trim().length > 2 && correoOK && celOK; const pct = Math.round((step / (TOTAL - 1)) * 100); const data = useMemoC(() => ({ prom3: num(sueldo), avg6: CCV.avgMid4(m6.map(num)), avg3: CCV.avgRecent(m6.map(num), 3), actividad, shares: num(shares) / 100, nominaBank, bonos: num(bonos), clienteBancos, buroFijos: num(fijos.hipoteca) + num(fijos.auto) + num(fijos.nomina) + num(fijos.personales), buroCCPago: "", buroTarjetas: num(tarjPago), }), [sueldo, m6, actividad, shares, nominaBank, bonos, clienteBancos, fijos, tarjPago]); const cap = useMemoC(() => (step === S.RESULT && engineType ? CCV.computeCapacity(engineType, data, prop, engPct, anios) : null), [step, engineType, data, prop, engPct, anios]); const perfil = useMemoC(() => (cap ? CCV.computePerfil(engineType, cap, { urg: intent.urg && intent.urg.s, dolor: intent.dolor && intent.dolor.s, comp: intent.comp && intent.comp.s, dec: intent.dec && intent.dec.s, acc: intent.acc && intent.acc.s, }, { engancheRatio: engPct }) : null), [cap, engineType, intent, engPct]); const aprobados = cap ? cap.results.filter((r) => r.disponible && r.aprobado) : []; const disponiblesNo = cap ? cap.results.filter((r) => r.disponible && !r.aprobado) : []; const mejorGap = disponiblesNo.length ? disponiblesNo.reduce((a, b) => (-b.margen < -a.margen ? b : a)) : null; const bestPago = aprobados[0] ? aprobados[0].pago : (cap && cap.results.find((r) => r.disponible) ? cap.results.find((r) => r.disponible).pago : 0); // Plazo recomendado: busca el plazo (hasta 20 años) con la mensualidad más baja aprobada const recPlazo = useMemoC(() => { if (step !== S.RESULT || !cap) return null; const opts = [15, 20].map((a) => { const c = CCV.computeCapacity(engineType, data, prop, engPct, a); const ap = c.results.filter((r) => r.disponible && r.aprobado); const any = c.results.find((r) => r.disponible); const pago = ap[0] ? ap[0].pago : (any ? any.pago : null); return { anios: a, pago }; }).filter((o) => o.pago != null); if (!opts.length) return null; const rec = opts.slice().sort((a, b) => a.pago - b.pago)[0]; return { anios: rec.anios, pago: rec.pago }; }, [step, cap, engineType, data, prop, engPct]); // Guardar lead (una sola vez por flujo) useEffectC(() => { if (step === S.RESULT && cap && perfil && !savedRef.current) { savedRef.current = true; const lead = { id: "L" + Date.now(), ts: Date.now(), estado: "nuevo", contacto, propiedad: { id: prop.id, titulo: prop.titulo, precio: prop.precio, ciudad: prop.ciudad }, agente: { nombre: "Juan Pérez", inmobiliaria: "Inmobiliaria del Puerto" }, tipoIngreso: type, engineType, ingreso: { sueldo: num(sueldo), m6: m6.map(num), actividad, shares: num(shares), nominaBank, bonos: num(bonos), clienteBancos }, ahorro: num(ahorro), enganchePct: engPct, idealMensual: 0, anios, deudas: { hipoteca: num(fijos.hipoteca), auto: num(fijos.auto), nomina: num(fijos.nomina), personales: num(fijos.personales), tarjPago: num(tarjPago), total: cap.buro.total }, intent, recPlazo, callback: { promesa: "En el transcurso del día", dueTs: (() => { const d = new Date(); d.setHours(23, 59, 0, 0); return d.getTime(); })() }, alerta: true, cap: { buro: cap.buro, results: cap.results }, perfil, }; try { const arr = JSON.parse(localStorage.getItem("ccv_leads") || "[]"); arr.unshift(lead); localStorage.setItem("ccv_leads", JSON.stringify(arr)); } catch (e) { /* noop */ } // Perfil hipotecario reutilizable: se guarda para calcular compatibilidad (Buyer Match) // en cualquier propiedad del portal, no solo en esta. if (window.CCVRank) { const prevBuyer = window.CCVRank.getBuyer() || {}; const ingresosCalc = engineType === "nomina" ? num(sueldo) : data.avg6; const buyerProfile = { ...prevBuyer, ingresos: ingresosCalc || prevBuyer.ingresos, presupuesto: prop.precio, tipo: prevBuyer.tipo || prop.tipo, ts: Date.now() }; window.CCVRank.saveBuyer(buyerProfile); if (onSaved) onSaved(buyerProfile); } } }, [step, cap, perfil]); function go(n) { setStep(Math.max(0, Math.min(TOTAL - 1, n))); } function pickType(k) { setType(k); if (k === "honorarios") setActividad("independiente"); go(S.INGRESO); } function pickIntent(key, label, s, next) { setIntent((p) => ({ ...p, [key]: { label, s } })); go(next); } return (
e.stopPropagation()}>
✨ Perfil Hipotecario Inteligente
{step < S.RESULT && (
{pct}% · {MSG[step]}
)}
{/* CONTACTO */} {step === S.CONTACTO && ( <>
Vamos a calcular tu crédito ideal
En 2 minutos sabrás tu mensualidad, los bancos que te aprueban y tu mejor estrategia para {prop.titulo}. ¿A dónde te enviamos tu simulación?
🔒 Usamos tus datos solo para enviarte tu análisis y asesoría.
)} {/* TIPO */} {step === S.TIPO && ( <>
¿Cómo compruebas tus ingresos?
Con esto elegimos el método de cálculo correcto para ti.
{TIPOS.map((o) => ( ))} )} {/* INGRESO */} {step === S.INGRESO && ( <> {type === "nomina" ? ( <>
¿Cuál es tu sueldo neto mensual?
Lo que recibes en tu cuenta cada mes, después de impuestos.
Algunos bancos suman tus bonos comprobables al ingreso, y otros elevan tu tope si tu nómina es de ellos.
) : ( <>
{type === "empresario" ? "Depósitos de tu empresa" : "Tus depósitos de los últimos 6 meses"}
{type === "empresario" ? "Lista los depósitos de los últimos 6 estados de cuenta de tu empresa (suma hasta 3 cuentas por mes). Eliminamos el mes más alto y el más bajo." : "Lista los depósitos que reflejan tus ingresos en tus últimos 6 estados de cuenta (suma hasta 3 cuentas por mes). Eliminamos el mes más alto y el más bajo."}
{type === "empresario" && ( )}
¿De cuáles de estos bancos ya eres cliente? (mejora tu tope en algunos)
{CCV.BANCOS.map((b) => { const on = clienteBancos.indexOf(b.nombre) >= 0; return ; })}
)}
)} {/* ENGANCHE */} {step === S.ENGANCHE && ( <>
¿Cuánto tienes ahorrado para el enganche?
Tu ahorro reduce el monto a financiar y baja tu mensualidad. Mientras más, mejores condiciones.
{num(ahorro) > 0 && (
Equivale al {Math.round(engPct * 100)}% del valor de la casa
)}
)} {/* FIJOS */} {step === S.FIJOS && ( <>
Tus pagos fijos mensuales
Lo que ya pagas cada mes en el Buró. Nos ayuda a calcular tu tope de endeudamiento real.
)} {/* TARJETAS */} {step === S.TARJETAS && ( <>
Tus tarjetas de crédito
Suma el saldo de TODAS tus tarjetas: lo que has usado y debes al día de hoy (no el límite). Con eso estimamos tu pago mensual.
Pago mensual estimado de tarjetas: {CCV.fmtMXN(num(tarjPago) * CCV.CC_FACTOR)} · Total de pagos fijos: {CCV.fmtMXN(data.buroFijos + num(tarjPago) * CCV.CC_FACTOR)}
)} {/* INTENCIÓN (camufladas) */} {step === S.URG && pickIntent("urg", l, s, S.DOLOR)} />} {step === S.DOLOR && pickIntent("dolor", l, s, S.COMP)} />} {step === S.COMP && pickIntent("comp", l, s, S.DEC)} />} {step === S.DEC && pickIntent("dec", l, s, S.ACC)} />} {step === S.ACC && pickIntent("acc", l, s, S.RESULT)} />} {/* RESULTADO (solo vista del cliente) */} {step === S.RESULT && cap && perfil && ( <> {aprobados.length > 0 ? ( <>
¡{contacto.nombre.trim().split(" ")[0] || "Listo"}, tu crédito es viable!
Tu perfil califica en {aprobados.length} {aprobados.length === 1 ? "banco" : "bancos"} para esta casa.
) : ( <>
!
{contacto.nombre.trim().split(" ")[0] || "Hola"}, tenemos una estrategia para ti
Con tus ingresos actuales aún no alcanzas el tope, pero hay camino.
)}
Mensualidad estimada{CCV.fmtMXN(bestPago)}
{recPlazo && (
✓ A {recPlazo.anios} años, tu mensualidad estimada es de {CCV.fmtMXN(recPlazo.pago)}.
)} {aprobados.length === 0 && mejorGap && (
👥 Suma a un coacreditado En {mejorGap.banco} te falta cubrir {CCV.fmtMXN(-mejorGap.margen)} de capacidad. Un coacreditado con buen Buró que aporte ~{CCV.fmtMXN((-mejorGap.margen) / mejorGap.topeFactor)} de ingreso mensual te alcanzaría.
)} {verDesglose && (
{cap.results.map((r) => (
{r.banco}{r.assumed && } {r.disponible ? {r.aprobado ? "Aprobado" : "No alcanza"} : No aplica}
{r.disponible ? (
Tope de pago{CCV.fmtMXN(r.tope)}
Mensualidad casa{CCV.fmtMXN(r.pago)}
Tus deudas{CCV.fmtMXN(r.buro)}
{r.aprobado ? "Te sobra" : "Te falta"}{CCV.fmtMXN(Math.abs(r.margen))}
) :
{r.motivo}
}
))}
)}
Ya tenemos tus datos (nombre, celular y correo): un asesor hipotecario te llamará en el transcurso del día para tu asesoría personalizada.
)}
); } window.CapacityFlow = CapacityFlow;