/* 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) => (
Mes {i + 1}
{ const a = [...values]; a[i] = e.target.value; setValues(a); }} />
))}
);
}
function ScoredQ({ q, onPick }) {
return (
<>
{q.q}
{q.sub && {q.sub}
}
{q.opts.map(([label, score]) => (
onPick(label, score)}>
{label}
→
))}
>
);
}
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 && (
)}
{/* 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?
Nombre completo
setContacto({ ...contacto, nombre: e.target.value })} />
Número celular (WhatsApp)
setContacto({ ...contacto, celular: e.target.value })} />
Correo electrónico
setContacto({ ...contacto, correo: e.target.value })} />
{ setTouched(true); if (contactoOK) go(S.TIPO); }}>Comenzar mi 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) => (
pickType(o.key)}>
{o.ic}
{o.t} {o.d}
))}
>
)}
{/* 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.
Sueldo neto mensual
setSueldo(e.target.value)} autoFocus />
¿En qué banco recibes tu nómina?
setNominaBank(e.target.value)}>
Selecciona tu banco…
{CCV.BANCOS.map((b) => {b.nombre} )}
Otro banco
Bonos mensuales demostrables (opcional)
setBonos(e.target.value)} />
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."}
Giro / actividad (cada banco aplica su propio factor)
setActividad(e.target.value)}>
{CCV.MASTER_ACTS.map((a) => {a.label} )}
{type === "empresario" && (
% de acciones que posees en la empresa
setShares(e.target.value)} />
)}
¿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 setClienteBancos((p) => on ? p.filter((x) => x !== b.nombre) : [...p, b.nombre])}>{b.nombre} ;
})}
>
)}
go(S.TIPO)}>← Atrás
go(S.ENGANCHE)}>Continuar →
>
)}
{/* 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.
Ahorro disponible para enganche
setAhorro(e.target.value)} autoFocus />
{num(ahorro) > 0 && (
Equivale al {Math.round(engPct * 100)}% del valor de la casa
)}
go(S.INGRESO)}>← Atrás
go(S.FIJOS)}>Continuar →
>
)}
{/* 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.
Hipoteca setFijos({ ...fijos, hipoteca: e.target.value })} />
Crédito de auto setFijos({ ...fijos, auto: e.target.value })} />
Crédito de nómina setFijos({ ...fijos, nomina: e.target.value })} />
Préstamos personales setFijos({ ...fijos, personales: e.target.value })} />
go(S.ENGANCHE)}>← Atrás
go(S.TARJETAS)}>Continuar →
>
)}
{/* 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.
Suma de saldos de tarjetas de crédito
setTarjPago(e.target.value)} autoFocus />
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)}
go(S.FIJOS)}>← Atrás
go(S.URG)}>Continuar →
>
)}
{/* 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.
)}
setVerDesglose(!verDesglose)}>{verDesglose ? "▲ Ocultar" : "▼ Ver"} desglose banco por banco
{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}
}
))}
)}
Quiero mi asesoría personalizada →
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;