/* =====================================================================
ed-quoter.jsx — motor genérico de cotizador (5 pasos: 4 preguntas + resultado).
Config-driven desde ED.quoters[key]. Cálculo puro y reproducible.
===================================================================== */
const EDP = window.ED.platforms;
/* ---- Cálculo puro ---- */
function edPlatformRange(cfg, platformKey) {
return (cfg.platforms || []).find(p => p.key === platformKey) || null;
}
function edScaleFactor(cfg, answers) {
// busca el primer paso single con 'factor' en sus opciones
for (const st of cfg.steps) {
if (st.kind === "single" && st.options.some(o => o.factor != null)) {
const v = answers[st.id];
const opt = st.options.find(o => o.value === v);
if (opt && opt.factor != null) return opt.factor;
}
}
return 0;
}
function edAddonsTotal(cfg, answers) {
let sum = 0; const picked = [];
for (const st of cfg.steps) {
if (st.kind === "multi") {
const arr = answers[st.id] || [];
arr.forEach(v => {
const opt = st.options.find(o => o.value === v);
if (opt) { sum += (opt.price || 0); if ((opt.price || 0) > 0) picked.push(opt); }
});
}
}
return { sum, picked };
}
function edCalc(cfg, answers) {
const pr = edPlatformRange(cfg, answers.plataforma);
if (!pr) return null;
const factor = edScaleFactor(cfg, answers);
// base = punto dentro (o más allá) del rango según el factor de escala
const base = Math.round(pr.min + Math.min(factor, 1) * (pr.max - pr.min) + Math.max(factor - 1, 0) * (pr.max - pr.min) * 0.6);
const { sum, picked } = edAddonsTotal(cfg, answers);
const total = base + sum;
const [tMin, tMax] = cfg.timeBase;
const days = [Math.round(tMin + factor * tMin * 0.6), Math.round(tMax + factor * tMax * 0.7)];
return {
base, addonsSum: sum, addonsPicked: picked, total,
min: Math.round(total * 0.95), max: Math.round(total * 1.12),
days, platform: pr, platformKey: answers.plataforma,
};
}
/* ---- Tarjeta de opción ---- */
function QOption({ selected, onClick, title, desc, multi, right }) {
const [hover, setHover] = useS(false);
return (
);
}
/* ---- Paso de plataforma (con rango de precio) ---- */
function QPlatformStep({ cfg, value, onPick }) {
return (
{cfg.platforms.map(p => {
const meta = EDP[p.key]; const sel = value === p.key;
return (
);
})}
);
}
/* ---- Resultado del cotizador ---- */
function QResult({ cfg, answers, calc, onEdit, onCalendly, onSubmit }) {
const meta = EDP[calc.platformKey];
return (
Tu estimado está listo
{cfg.title.replace("Cotizador de ", "")} en {meta ? meta.label : ""}
Inversión estimada
${calc.min.toLocaleString("en-US")} – ${calc.max.toLocaleString("en-US")}
USD · según alcance final
Tiempo de entrega
{calc.days[0]}–{calc.days[1]} días
hábiles
{/* Desglose */}
¿Qué incluye este estimado?
Construcción en {meta ? meta.label : ""}
${calc.base.toLocaleString("en-US")}
{calc.addonsPicked.map((a, i) => (
{a.label}
+${a.price}
))}
Compliance legal · Términos, Privacidad y Cookies — incluido
Ajustar respuestas
Agendar llamada
Solicitar cotización formal
);
}
/* ---- Cotizador (orquestador) ---- */
function Quoter({ cfgKey }) {
const cfg = window.ED.quoters[cfgKey];
const storeKey = "ed_quoter_" + cfgKey;
const [answers, setAnswers] = useS(() => {
try { return JSON.parse(localStorage.getItem(storeKey)) || {}; } catch (e) { return {}; }
});
const [step, setStep] = useS(0); // 0..3 preguntas, 4 = resultado
const [error, setError] = useS(null);
const [showCal, setShowCal] = useS(false);
const total = cfg.steps.length; // 4 pasos de pregunta
useE(() => { try { localStorage.setItem(storeKey, JSON.stringify(answers)); } catch (e) {} }, [answers]);
useE(() => { setError(null); if (window.lucide) window.lucide.createIcons(); }, [step]);
function setSingle(stepId, value) { setAnswers(a => Object.assign({}, a, { [stepId]: value })); }
function toggleMulti(stepId, value) {
setAnswers(a => { const arr = a[stepId] || []; return Object.assign({}, a, { [stepId]: arr.includes(value) ? arr.filter(v => v !== value) : arr.concat(value) }); });
}
function validate(i) {
const st = cfg.steps[i];
if (st.kind === "multi") return { ok: true };
if (!answers[st.id]) return { ok: false, error: "Selecciona una opción para continuar." };
return { ok: true };
}
function next() {
if (step < total) { const v = validate(step); if (!v.ok) { setError(v.error); return; } }
setStep(s => Math.min(s + 1, total));
}
function back() { setStep(s => Math.max(s - 1, 0)); }
const calc = edCalc(cfg, answers);
const isResult = step >= total;
const st = !isResult ? cfg.steps[step] : null;
// ── Submit al backend Laravel ──
const [showContact, setShowContact] = useS(false);
const [contact, setContact] = useS({ nombre: "", email: "", whatsapp: "" });
const [sending, setSending] = useS(false);
const [sent, setSent] = useS(false);
async function submitToBackend(e) {
e?.preventDefault?.();
if (!contact.nombre || !contact.email || !contact.whatsapp) return;
setSending(true);
const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
const url = (window.SELLU && window.SELLU.cotizarUrl) || "/cotizador/enviar";
const platformKey = answers.plataforma || (calc && calc.platformKey);
const platformLabel = platformKey && window.ED.platforms[platformKey]
? window.ED.platforms[platformKey].label
: platformKey || "—";
const fd = new FormData();
fd.append("_token", csrf || "");
fd.append("tipo_proyecto", cfgKey);
fd.append("plataforma", platformLabel);
fd.append("paginas", "1");
fd.append("mantenimiento", "ninguno");
fd.append("precio_estimado", String(calc ? calc.mid : 0));
fd.append("urgencia", "cotizando");
fd.append("whatsapp", contact.whatsapp);
if (!window.SELLU?.isAuth) {
fd.append("guest_nombre", contact.nombre);
fd.append("guest_email", contact.email);
}
fd.append("notas", JSON.stringify({
folio: "ED-" + Date.now().toString(36).toUpperCase(),
tipo: cfgKey,
respuestas: answers,
plataforma: platformKey,
addons: calc?.addonsPicked || [],
rango: calc ? { min: calc.min, mid: calc.mid, max: calc.max } : null,
dias: calc ? calc.days : null,
contacto: contact,
}));
try {
await fetch(url, { method: "POST", body: fd, credentials: "same-origin" });
setSent(true);
setTimeout(() => setShowContact(false), 2200);
} catch (e) {
console.warn("No se pudo enviar al backend", e);
setSent(true); // mostramos confirmación igual; el localStorage guarda el folio
} finally {
setSending(false);
}
}
return (
{/* Progreso */}
{isResult ? "Resultado" : "Paso " + (step + 1) + " de " + total}
{cfg.title.replace("Cotizador de ", "")}
{isResult ? (
setStep(total - 1)} onCalendly={() => setShowCal(true)} onSubmit={() => setShowContact(true)} />
) : (
{st.title}
{st.subtitle &&
{st.subtitle}
}
{st.kind === "platform"
? setSingle("plataforma", k)} />
: st.options.map(opt => (
st.kind === "multi" ? toggleMulti(st.id, opt.value) : setSingle(st.id, opt.value)}
title={opt.label} desc={opt.desc}
right={opt.price != null ? (opt.price === 0 ? "Incluido" : "+$" + opt.price) : null} />
))}
)}
{/* Footer con estimado en vivo */}
{!isResult && (
{error &&
{error}
}
Atrás
{calc ? (
Estimado
${calc.min.toLocaleString("en-US")} – ${calc.max.toLocaleString("en-US")}
) :
Elige una plataforma para ver tu estimado
}
{step === total - 1 ? "Ver estimado" : "Siguiente"}
)}
setShowCal(false)} context={cfg.title.replace("Cotizador de ", "")} />
{/* Modal de contacto: cliente solicita cotización formal */}
{showContact && (
{ if (e.target === e.currentTarget && !sending) setShowContact(false); }}
style={{ position: "fixed", inset: 0, zIndex: 300, background: "rgba(20,24,42,.45)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }}>
)}
);
}
Object.assign(window, { Quoter, edCalc });