/* ===================================================================== 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 }}>
{!sent ? (

Recibe tu cotización formal

Te enviamos el detalle de tu proyecto y un especialista te contacta en menos de 24 horas.

setContact(c => ({...c, nombre: e.target.value}))} /> setContact(c => ({...c, email: e.target.value}))} /> setContact(c => ({...c, whatsapp: e.target.value}))} />
) : (

¡Cotización enviada!

Te responderemos a {contact.email} en menos de 24 horas hábiles.

)}
)}
); } Object.assign(window, { Quoter, edCalc });