/* =====================================================================
app.jsx — Cotizador Sell-U integrado dentro del layout público.
Sin barra top propia · sin altura 100vh · navegación inline tipo doc.
===================================================================== */
(function () {
const Cz = window.Cotizador;
const QUOTES_KEY = "sellu_quotes_v1";
function loadQuotes() {
try { return JSON.parse(localStorage.getItem(QUOTES_KEY) || "{}"); } catch (e) { return {}; }
}
function saveQuote(id, state) {
const all = loadQuotes();
all[id] = { state: JSON.parse(JSON.stringify(state)), createdAt: Date.now() };
try { localStorage.setItem(QUOTES_KEY, JSON.stringify(all)); } catch (e) {}
}
const STEP_TITLES = ["Tipo", "Escala", "Plataforma", "Estado actual", "Contenido", "Integraciones", "Soporte"];
/* ---- Barra de progreso compacta (chips) ---- */
function Progress({ step, canGoTo, onJump }) {
return (
{STEP_TITLES.map((t, i) => {
const n = i + 1;
const isDone = n < step;
const isCurrent = n === step;
const reachable = canGoTo(n);
return (
{n < 7 && }
);
})}
);
}
/* ---- App principal ---- */
function App() {
const store = window.useQuoteStore();
const { state, setField, setNested, toggleInArray, goTo, reset } = store;
const density = "regular";
const [error, setError] = React.useState(null);
const [showContact, setShowContact] = React.useState(false);
const [showGuestModal, setShowGuestModal] = React.useState(false);
const [submitting, setSubmitting] = React.useState(false);
const [guestForm, setGuestForm] = React.useState({ nombre: "", email: "", whatsapp: "" });
const stepHeaderRef = React.useRef(null);
const isAuth = !!window.SELLU?.isAuth;
React.useEffect(() => { if (window.lucide) window.lucide.createIcons(); });
React.useEffect(() => {
setError(null);
// Scroll suave al inicio del wizard cuando cambia el paso
if (stepHeaderRef.current) {
const offset = stepHeaderRef.current.getBoundingClientRect().top + window.scrollY - 90;
window.scrollTo({ top: offset, behavior: "smooth" });
}
}, [state.step, state.submittedId]);
function canGoTo(target) {
if (target <= state.step) return true;
for (let s = state.step; s < target; s++) {
if (!window.validateStep(s, state).ok) return false;
}
return true;
}
async function submitToBackend(state, quoteId, guestContact) {
const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
const url = window.SELLU?.cotizarUrl || "/cotizador/enviar";
const quote = Cz.calculateQuote(state);
const time = Cz.calculateTime(state);
const platform = Cz.PLATFORMS[state.platform];
const payload = {
_token: csrf,
tipo_proyecto: state.projectType,
plataforma: platform ? platform.label : state.platform,
paginas: Cz.pagesCount(state),
addons: (state.integrations || []),
mantenimiento: state.support || 'ninguno',
precio_estimado: quote ? quote.mid : 0,
urgencia: 'cotizando',
whatsapp: (guestContact && guestContact.whatsapp) || state.whatsapp || '—',
notas: JSON.stringify({
folio: quoteId,
tipo: state.projectType,
escala: state.scale,
plataforma: state.platform,
recomendada: Cz.recommendPlatform(state.projectType, state.scale)?.platform,
hosting: state.hostingOwnership,
estado_actual: state.currentStatus,
contenido: state.content,
integraciones: state.integrations,
soporte: state.support,
rango: quote ? { min: quote.min, mid: quote.mid, max: quote.max } : null,
tiempo_dias: time,
}),
};
if (!isAuth && guestContact) {
payload.guest_nombre = guestContact.nombre;
payload.guest_email = guestContact.email;
}
const form = new FormData();
Object.entries(payload).forEach(([k, v]) => {
if (Array.isArray(v)) v.forEach(x => form.append(k + '[]', x));
else form.append(k, v);
});
try {
await fetch(url, { method: 'POST', body: form, credentials: 'same-origin' });
} catch (e) {
console.warn('Cotización guardada local pero no se pudo enviar al servidor', e);
}
}
async function finalizar(guestContact) {
setSubmitting(true);
const id = Cz.makeQuoteId();
saveQuote(id, state);
await submitToBackend(state, id, guestContact);
setField({ submittedId: id });
setSubmitting(false);
setShowGuestModal(false);
}
async function next() {
const v = window.validateStep(state.step, state);
if (!v.ok) { setError(v.error); return; }
setError(null);
if (state.step < 7) {
goTo(state.step + 1);
} else {
if (!isAuth) {
setShowGuestModal(true);
return;
}
await finalizar(null);
}
}
function back() {
if (state.submittedId) { setField({ submittedId: null }); return; }
if (state.step > 1) goTo(state.step - 1);
}
function editStep(s) { setField({ submittedId: null }); goTo(s); }
function restart() { reset(); }
function submitGuest(e) {
e.preventDefault();
if (!guestForm.nombre || !guestForm.email || !guestForm.whatsapp) {
setError("Completa nombre, email y WhatsApp para enviar tu cotización.");
return;
}
finalizar(guestForm);
}
// ── Resultado ──
if (state.submittedId) {
return (
);
}
// ── Estimado dinámico (preview en el footer) ──
let estimate = null, estimatePrelim = false;
if (state.step >= 2 && state.projectType && state.scale) {
const q = Cz.calculateQuote(state, state.platform || "wordpress-elementor");
if (q) { estimate = q; estimatePrelim = !state.platform; }
}
const StepComp = [Step1, Step2, Step3, Step4, Step5, Step6, Step7][state.step - 1];
const stepProps = { state, setField, setNested, toggleInArray, density, openContact: () => setShowContact(true) };
return (
{/* Barra de progreso */}
{/* Step actual */}
{/* Footer fijo con CTA */}
{error && (
{error}
)}
{estimate ? (
{estimatePrelim ? "Estimado preliminar" : "Estimado"}
{Cz.fmtUSD(estimate.min)} – {Cz.fmtUSD(estimate.max)}
) :
}
setShowContact(false)} />
{/* Modal contacto guest */}
{showGuestModal && (
{ if (e.target === e.currentTarget && !submitting) setShowGuestModal(false); }} style={{
position: "fixed", inset: 0, zIndex: 300, background: "rgba(20,24,42,.4)",
display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
}}>
)}
);
}
window.CotizadorApp = App;
})();