/* ===================================================================== quoteStore — estado global del wizard con persistencia (localStorage). Análogo a Zustand: un único store + selector hook + setters. Validación por paso análoga a Zod: validateStep(step, state) -> {ok, error}. Expone: window.useQuoteStore, window.validateStep, window.EMPTY_STATE ===================================================================== */ (function () { "use strict"; const { useState, useEffect, useCallback, useRef } = React; const STORAGE_KEY = "sellu_cotizador_v1"; const EMPTY_STATE = { step: 1, // 1..7 (8 = resultado) projectType: null, // 'landing' | 'sales' | 'funnel' scale: null, // depende de projectType platform: null, // PlatformKey platformConfirmed: false, // para flujo "Recomiéndame" recommendChosen: false, // marcó la tarjeta "Recomiéndame" hostingOwnership: null, // 'client' | 'sell-u' currentStatus: { hasWebsite: null, // 'no' | 'migrate' | 'redesign' hasDomain: null, // 'yes' | 'no' | 'unknown' hasHosting: null, // boolean hasBranding: null, // 'no' | 'partial' | 'complete' }, content: { productPhotos: null, // 'none' | 'some' | 'all-pro' copy: null, // 'client' | 'sell-u' videos: null, // 'none' | 'has' | 'needs-production' languages: 1, // 1 | 2 | 3 }, integrations: [], // string[] support: null, // 'none' | '3m-basic' | '6m-standard' | '12m-premium' addOns: [], submittedId: null, // folio generado al finalizar (vista resultado) }; function deepClone(o) { return JSON.parse(JSON.stringify(o)); } function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return deepClone(EMPTY_STATE); const parsed = JSON.parse(raw); // merge defensivo con EMPTY_STATE return Object.assign(deepClone(EMPTY_STATE), parsed, { currentStatus: Object.assign({}, EMPTY_STATE.currentStatus, parsed.currentStatus || {}), content: Object.assign({}, EMPTY_STATE.content, parsed.content || {}), }); } catch (e) { return deepClone(EMPTY_STATE); } } // ---- Validación por paso (Zod-style) ---------------------------------- function validateStep(step, s) { switch (step) { case 1: if (!s.projectType) return { ok: false, error: "Elige un tipo de proyecto para continuar." }; return { ok: true }; case 2: if (!s.scale) return { ok: false, error: "Selecciona el tamaño de tu proyecto." }; return { ok: true }; case 3: if (!s.platform) return { ok: false, error: "Elige una plataforma o pide una recomendación." }; if (s.recommendChosen && !s.platformConfirmed) return { ok: false, error: "Confirma la plataforma recomendada para avanzar." }; if (!s.hostingOwnership) return { ok: false, error: "Indica a nombre de quién queda el hosting/licencia." }; return { ok: true }; case 4: { const cs = s.currentStatus || {}; if (!cs.hasWebsite) return { ok: false, error: "Cuéntanos si ya tienes página web." }; if (!cs.hasDomain) return { ok: false, error: "Indica si tienes dominio." }; if (cs.hasHosting == null) return { ok: false, error: "Indica si tienes hosting activo." }; if (!cs.hasBranding) return { ok: false, error: "Indica el estado de tu branding." }; return { ok: true }; } case 5: { const co = s.content || {}; if (!co.productPhotos) return { ok: false, error: "Selecciona el estado de tus fotos de producto." }; if (!co.copy) return { ok: false, error: "Indica quién redacta los textos." }; if (!co.videos) return { ok: false, error: "Indica el estado de tus videos." }; if (!co.languages) return { ok: false, error: "Selecciona el número de idiomas." }; return { ok: true }; } case 6: // Integraciones es opcional (multi-select). Siempre válido. return { ok: true }; case 7: if (!s.support) return { ok: false, error: "Elige una opción de soporte post-entrega." }; return { ok: true }; default: return { ok: true }; } } // ---- Hook de store ----------------------------------------------------- // Patrón: estado local + persistencia. Un único proveedor en App. function useQuoteStore() { const [state, setStateRaw] = useState(loadState); const stateRef = useRef(state); stateRef.current = state; // persistir en cada cambio useEffect(() => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} }, [state]); // setField('projectType', 'landing') o setField({ projectType:'landing', scale:null }) const setField = useCallback((keyOrObj, value) => { setStateRaw(prev => { const next = deepClone(prev); if (typeof keyOrObj === "object") { Object.assign(next, keyOrObj); } else { next[keyOrObj] = value; } return next; }); }, []); // setNested('content', 'languages', 2) const setNested = useCallback((group, key, value) => { setStateRaw(prev => { const next = deepClone(prev); next[group] = Object.assign({}, next[group], { [key]: value }); return next; }); }, []); const toggleInArray = useCallback((group, value) => { setStateRaw(prev => { const next = deepClone(prev); const arr = next[group] || []; next[group] = arr.includes(value) ? arr.filter(v => v !== value) : arr.concat(value); return next; }); }, []); const goTo = useCallback((step) => { setStateRaw(prev => Object.assign(deepClone(prev), { step })); }, []); const reset = useCallback(() => { const fresh = deepClone(EMPTY_STATE); setStateRaw(fresh); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh)); } catch (e) {} }, []); return { state, setField, setNested, toggleInArray, goTo, reset, setStateRaw }; } window.useQuoteStore = useQuoteStore; window.validateStep = validateStep; window.EMPTY_STATE = EMPTY_STATE; window.QUOTE_STORAGE_KEY = STORAGE_KEY; })();