const { useState, useMemo, useEffect } = React; const DEFAULTS = { nombre: "Departamento en preventa", ubicacion: "CDMX", precio: 4500000, engPct: 10, mensPct: 30, numMensualidades: 24, entregaPct: 60, escrituraPct: 7, // ROI tarifaProm: 2400, ocupacion: 70, comisionPct: 15, adminPct: 5, gastosFijos: 4500, plusvalia: 10, inflacionRenta: 5, horizonte: 10, usarCredito: true, tasaCredito: 11.5, plazoCredito: 20, }; function App() { const { calcular, fmtMXN, fmtPct, fmtNum } = window.CalcLogic; const [tweaks, setTweaksLocal] = useState({ accent: "bronce", modoVista: "analisis" }); useEffect(() => { window.__appSetTweak = (k, v) => setTweaksLocal((s) => ({ ...s, [k]: v })); return () => { delete window.__appSetTweak; }; }, []); useEffect(() => { const accents = { bronce: "oklch(0.55 0.08 95)", indigo: "oklch(0.45 0.10 260)", bosque: "oklch(0.45 0.07 150)", vino: "oklch(0.45 0.10 25)", }; document.documentElement.style.setProperty("--accent", accents[tweaks.accent] || accents.bronce); }, [tweaks.accent]); const [inputs, setInputs] = useState(DEFAULTS); const set = (k) => (v) => setInputs((s) => ({ ...s, [k]: v })); const setNum = (k) => (v) => setInputs((s) => ({ ...s, [k]: v === "" ? 0 : +v })); const r = useMemo(() => calcular(inputs), [inputs]); const roi = useMemo(() => window.CalcLogic.calcularROI( r.precio, r.contraEntrega, r.escrituracion, r.totalAPagar, inputs ), [r, inputs]); // Auto-balancear: si suma != 100, calcula faltante para sugerir const faltante = 100 - (inputs.engPct + inputs.mensPct + inputs.entregaPct); const setTweak = (k, v) => { setTweaksLocal((s) => ({ ...s, [k]: v })); window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [k]: v } }, '*'); }; const presentMode = tweaks.modoVista === "presentacion"; return (
{!presentMode && ( )}
{!presentMode && ( )}
); } function Disclaimer() { return (
Nota importante

Este documento es una simulación de carácter informativo y meramente ilustrativo. Las cifras aquí presentadas son estimaciones basadas en los datos proporcionados y las condiciones actuales del mercado. Los valores reales, tasas de interés, gastos notariales y rendimientos finales pueden variar al momento de la operación formal. AZ Real Estate no se hace responsable por variaciones en los resultados finales de la inversión.

); } function Header({ inputs, setInputs, presentMode, setTweak }) { return (
AZ Real Estate
AZ Real Estate
Calculadora de Rentabilidad Inmobilairia
setInputs((s) => ({ ...s, nombre: e.target.value }))} /> setInputs((s) => ({ ...s, ubicacion: e.target.value }))} />
); } function InputsForm({ inputs, set, setNum, faltante, balanceadoOk }) { const { fmtMXN, fmtPct } = window.CalcLogic; const precio = +inputs.precio || 0; return (
{ }} prefix="$" hint="Calculado automáticamente" />
{!balanceadoOk && (
0 ? "balance--less" : "balance--more")}> {faltante > 0 ? `Falta ${fmtPct(faltante, 1)} para completar el 100%` : `Excede ${fmtPct(-faltante, 1)} del precio total`}
)}

Incluye notario, ISAI, avalúo y RPP. No forma parte del precio del depa.

); } function Section({ title, badge, badgeOk, children }) { return (
{title}
{badge !== undefined && ( {badge} )}
{children}
); } function Results({ r, inputs, faltante }) { const { fmtMXN, fmtPct } = window.CalcLogic; return (
Informe del esquema de pagos

Inversión total de {fmtMXN(r.totalAPagar, { compact: true })}

Sobre {fmtMXN(r.precio)} de precio del inmueble más {fmtMXN(r.escrituracion)} de gastos de escrituración ({fmtPct(inputs.escrituraPct, 1)}). {!r.balanceadoOk && ( {" "}⚠ El esquema {faltante > 0 ? "no llega" : "excede"} el 100% por {fmtPct(Math.abs(faltante), 1)}. )}

{/* KPIs principales */}
{/* Distribución */}
Distribución del precio
{/* Desglose detallado */}
Desglose completo
Pagos del inmueble
Enganche {fmtPct(inputs.engPct, 1)} {fmtMXN(r.enganche)}
Mensualidades · {r.numMensualidades} pagos de {fmtMXN(r.mensualidad)} {fmtPct(inputs.mensPct, 1)} {fmtMXN(r.totalMensualidades)}
Saldo contra entrega {fmtPct(inputs.entregaPct, 1)} {fmtMXN(r.contraEntrega)}
Subtotal precio {fmtPct(inputs.engPct + inputs.mensPct + inputs.entregaPct, 1)} {fmtMXN(r.totalSinEscritura)}
Gastos adicionales
Escrituración {fmtPct(inputs.escrituraPct, 1)} {fmtMXN(r.escrituracion)}
Total a desembolsar {fmtMXN(r.totalAPagar)}
{/* Cronograma */}
Cronograma de pagos
{r.cronograma.map((p, i) => ( ))}
Mes Concepto % del precio Pago Acumulado % Acum.
{p.mes === 0 ? "Firma" : `M${p.mes}`}
{p.concepto}
{p.detalle}
{fmtPct(p.porcentaje, 2)} {fmtMXN(p.pago)} {fmtMXN(p.acumulado)} {fmtPct(p.porcentajeAcum, 1)}
); } function PrintFooter({ inputs }) { const today = new Date().toLocaleDateString("es-MX", { day: "numeric", month: "long", year: "numeric" }); return ( ); } function ROIInputs({ inputs, set, setNum }) { const { fmtMXN } = window.CalcLogic; return (
Después de la entrega

Rentabilidad y plusvalía

Configura los supuestos de renta vacacional para proyectar el ROI

Renta vacacional
Proyección
{inputs.usarCredito && (
Crédito hipotecario

El crédito se calcula sobre el saldo contra entrega ({fmtMXN(inputs.precio * (inputs.entregaPct / 100))}).

)}
); } function ROIResults({ roi, r, inputs }) { const { fmtMXN, fmtPct } = window.CalcLogic; const flujoTone = roi.flujoNetoMensual >= 0 ? "positive" : "negative"; // Cobertura de la hipoteca let coberturaCopy = ""; let coberturaTone = "neutral"; if (roi.usarCredito) { if (roi.cubrePctHip === null) coberturaCopy = "—"; else if (roi.cubrePctHip >= 100) { coberturaCopy = `Tu renta cubre el ${fmtPct(roi.cubrePctHip, 0)} de la hipoteca`; coberturaTone = "positive"; } else { coberturaCopy = `Tu renta cubre el ${fmtPct(roi.cubrePctHip, 0)} de la hipoteca`; coberturaTone = "warn"; } } return (
Proyección a {roi.horizonte} años

Tu inversión generaría {fmtMXN(roi.gananciaTotal, { compact: true })} de ganancia total

Con tarifa promedio de {fmtMXN(roi.tarifaProm)} por noche al{" "} {fmtPct(roi.ocupacion, 0)} de ocupación, plusvalía anual de{" "} {fmtPct(roi.plusvalia, 1)} {roi.usarCredito && (<>{" "}y crédito a {fmtPct(roi.tasaCredito, 1)} por {roi.plazoCredito} años)}.

{/* Comparativo apalancamiento */}
Apalancamiento: con crédito vs. sin crédito
Con crédito (apalancado)
{fmtPct(roi.roiTotal, 1)}
{fmtPct(roi.roiAnualizado, 1)} anualizado
Pones de tu bolsa
{fmtMXN(roi.inversionPropia)}
Flujo mensual
= 0 ? "is-pos" : "is-neg"}>{fmtMXN(roi.flujoNetoMensual)}
Ganancia total a {roi.horizonte} años
{fmtMXN(roi.gananciaTotal)}

El banco aporta el saldo contra entrega. Tu rendimiento se dispara porque controlas un activo grande con poco capital propio — pero el flujo mensual puede ser negativo.

Sin crédito (de contado)
{fmtPct(roi.roiSinApal, 1)}
{fmtPct(roi.roiAnualSinApal, 1)} anualizado
Pones de tu bolsa
{fmtMXN(roi.inversionSinApal)}
Flujo mensual
{fmtMXN(roi.flujoMensualSinApal)}
Ganancia total a {roi.horizonte} años
{fmtMXN((roi.proyeccion[roi.proyeccion.length - 1].valor - r.precio) + (roi.flujoMensualSinApal * 12 * roi.horizonte))}

Pagas todo de contado. El flujo es positivo desde el día 1, pero el ROI es menor porque tu capital invertido es mucho mayor.

{/* Se paga solo */} {roi.usarCredito && (
Tu crédito se paga solo
{coberturaCopy}
Renta neta
{fmtMXN(roi.ingresoNetoOp)}
Pago hipoteca
{fmtMXN(roi.pagoMensualHip)}
)} {/* Desglose mensual operativo */}
Flujo mensual de operación
{roi.usarCredito && ( )}
Ingreso bruto · {fmtNum(roi.noches, 1)} noches × {fmtMXN(roi.tarifaProm)} 100% {fmtMXN(roi.ingresoBruto)}
Comisión plataforma −{fmtPct(inputs.comisionPct, 0)} −{fmtMXN(roi.comision)}
Administración −{fmtPct(inputs.adminPct, 0)} −{fmtMXN(roi.admin)}
Gastos fijos −{fmtMXN(roi.gastosFijos)}
Renta neta operativa {fmtMXN(roi.ingresoNetoOp)}
Pago hipoteca · {fmtPct(roi.tasaCredito, 1)} × {roi.plazoCredito} años −{fmtMXN(roi.pagoMensualHip)}
Flujo neto al inversionista {fmtMXN(roi.flujoNetoMensual)}
{/* Gráfica de proyección */} {/* Tabla año a año */}
Proyección año a año
{roi.usarCredito && } {roi.usarCredito && } {roi.proyeccion.map((p) => ( {roi.usarCredito && } {roi.usarCredito && } ))}
Año Ingreso operativoPago hipotecaFlujo del año Flujo acumulado Valor del depaSaldo créditoEquity
Año {p.anio} {fmtMXN(p.ingresoOp)}−{fmtMXN(p.pagoHip)}= 0 ? "num--strong" : "num--neg")}>{fmtMXN(p.flujoNeto)} {fmtMXN(p.flujoAcumulado)} {fmtMXN(p.valor)}{fmtMXN(p.saldo)}{fmtMXN(p.equity)}
); } function ROIChart({ proyeccion, precio, inversion }) { const { fmtMXN } = window.CalcLogic; const w = 900, h = 320, padL = 80, padR = 16, padT = 24, padB = 36; const data = [{ anio: 0, valor: precio, equity: precio - (proyeccion[0]?.saldo > 0 ? (precio * 0.6) : 0), flujoAcumulado: -inversion }, ...proyeccion]; // Simplificar: usar proyeccion sin el punto 0 sintético const d = proyeccion; const allVals = d.flatMap(p => [p.valor, p.equity, p.flujoAcumulado]); const min = Math.min(0, ...allVals); const max = Math.max(...allVals); const range = max - min || 1; const xFor = (i) => padL + (i / Math.max(1, d.length - 1)) * (w - padL - padR); const yFor = (v) => padT + (1 - (v - min) / range) * (h - padT - padB); const pathFor = (key) => d.map((p, i) => `${i === 0 ? "M" : "L"} ${xFor(i)} ${yFor(p[key])}`).join(" "); const ticks = 5; const tickValues = Array.from({ length: ticks + 1 }, (_, i) => min + (range * i) / ticks); return (
Cómo crece tu inversión
Valor del depa, equity y flujo acumulado
Valor del depa Equity Flujo acumulado
{tickValues.map((v, i) => ( {fmtMXN(v, { compact: true })} ))} {min < 0 && } {d.map((p, i) => ( (i === 0 || i === d.length - 1 || i % Math.ceil(d.length / 6) === 0) && ( Año {p.anio} ) ))}
); } ReactDOM.createRoot(document.getElementById("root")).render();