`; const win = window.open('', '_blank'); win.document.write(html); win.document.close(); } function escapeHTML(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/\n/g, '
'); } // ============ HELPERS ============ function h(tag, attrs = {}, ...children) { const el = document.createElement(tag); Object.entries(attrs || {}).forEach(([k, v]) => { if (k === 'class') el.className = v; else if (k === 'html') el.innerHTML = v; else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v); else if (v !== null && v !== undefined && v !== false) el.setAttribute(k, v); }); children.flat().forEach(c => { if (c == null || c === false) return; el.appendChild(typeof c === 'string' || typeof c === 'number' ? document.createTextNode(c) : c); }); return el; } function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); } function calcularScores() { const areas = { estrategia: [], ejecucion: [], gestion: [] }; APTITUDES.forEach(a => { const val = state.aptitudes[a.id]; if (val != null) areas[a.area].push(val); }); const avg = arr => arr.length ? arr.reduce((s, n) => s + n, 0) / arr.length : 0; return { estrategia: avg(areas.estrategia), ejecucion: avg(areas.ejecucion), gestion: avg(areas.gestion), total: avg([...areas.estrategia, ...areas.ejecucion, ...areas.gestion]) }; } function aptitudesBajas() { return APTITUDES.filter(a => (state.aptitudes[a.id] || 0) > 0 && state.aptitudes[a.id] <= 2); } function setStep(n) { state.step = n; saveState(); render(); window.scrollTo({ top: 0, behavior: 'smooth' }); } function setTab(tab) { state.currentTab = tab; saveState(); render(); window.scrollTo({ top: 0, behavior: 'smooth' }); } // ============ MODAL INFRASTRUCTURE ============ let _currentModal = null; function openModal(title, contentEl, footerEl) { closeModal(); const overlay = h('div', { class: 'modal-overlay' }); const modal = h('div', { class: 'modal' }, h('div', { class: 'modal-header' }, h('div', { class: 'modal-title' }, title), h('button', { class: 'modal-close', onclick: closeModal }, '×') ), h('div', { class: 'modal-body' }, contentEl), footerEl ? h('div', { class: 'modal-footer' }, footerEl) : null ); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); overlay.appendChild(modal); document.body.appendChild(overlay); _currentModal = overlay; document.addEventListener('keydown', _modalEscHandler); } function _modalEscHandler(e) { if (e.key === 'Escape') closeModal(); } function closeModal() { if (_currentModal) { _currentModal.remove(); _currentModal = null; document.removeEventListener('keydown', _modalEscHandler); } } // ============ TASK EDITOR ============ function openTaskEditor(task, isNew, onDone) { const data = { titulo: task.titulo || '', hora: task.hora || '', duracion_min: task.duracion_min || 30, dia: task.dia || 'lunes', categoria_actividad: task.categoria_actividad || 'planificacion', detalle: task.detalle || '', }; const form = h('div', {}, h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Título'), h('input', { class: 'input', type: 'text', value: data.titulo, oninput: e => data.titulo = e.target.value }) ), h('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:12px' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Hora'), h('input', { class: 'input', type: 'time', value: data.hora, oninput: e => data.hora = e.target.value }) ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Duración'), (() => { const sel = h('select', { class: 'select', onchange: e => data.duracion_min = parseInt(e.target.value) }, ...[10, 15, 30, 45, 60, 90, 120].map(v => { const o = h('option', { value: String(v) }, v + ' min'); if (v === data.duracion_min) o.selected = true; return o; }) ); return sel; })() ) ), h('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:12px' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Día'), (() => { const sel = h('select', { class: 'select', onchange: e => data.dia = e.target.value }, ...DIAS.map(d => { const o = h('option', { value: d }, d); if (d === data.dia) o.selected = true; return o; }) ); return sel; })() ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Categoría'), (() => { const sel = h('select', { class: 'select', onchange: e => data.categoria_actividad = e.target.value }, ...CATEGORIAS.map(c => { const o = h('option', { value: c.id }, c.icon + ' ' + c.label); if (c.id === data.categoria_actividad) o.selected = true; return o; }) ); return sel; })() ) ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Detalle'), h('textarea', { class: 'textarea', oninput: e => data.detalle = e.target.value }, data.detalle) ) ); const footerBtns = h('div', { style: 'display:flex;gap:10px;width:100%;justify-content:space-between;flex-wrap:wrap' }); const leftBtns = h('div', { style: 'display:flex;gap:8px' }); if (!isNew) { leftBtns.appendChild(h('button', { class: 'btn btn-ghost btn-sm', style: 'color:var(--terra)', onclick: () => { if (confirm('¿Eliminar esta tarea?')) { removeTask(task.id); closeModal(); if (onDone) onDone(); render(); } } }, '🗑 Eliminar')); if (isTaskOverridden(task.id) && task.origen !== 'usuario') { leftBtns.appendChild(h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { resetTaskOverrides(task.id); closeModal(); if (onDone) onDone(); render(); } }, '↩ Restaurar')); } } footerBtns.appendChild(leftBtns); footerBtns.appendChild(h('button', { class: 'btn btn-primary btn-sm', onclick: () => { if (!data.titulo.trim()) return; if (isNew) { addUserTask(data.dia, data); } else if (task.origen === 'usuario') { // For user tasks, update the add override directly const addOv = state.cronograma_overrides.find(o => o.op === 'add' && o.task && o.task.id === task.id); if (addOv) Object.assign(addOv.task, data); saveState(); } else { const changes = {}; if (data.titulo !== task.titulo) changes.titulo = data.titulo; if (data.hora !== task.hora) changes.hora = data.hora; if (data.duracion_min !== task.duracion_min) changes.duracion_min = data.duracion_min; if (data.categoria_actividad !== task.categoria_actividad) changes.categoria_actividad = data.categoria_actividad; if (data.detalle !== task.detalle) changes.detalle = data.detalle; if (data.dia !== task.dia) { moveTask(task.id, data.dia, data.hora); } if (Object.keys(changes).length > 0) editTask(task.id, changes); } closeModal(); if (onDone) onDone(); render(); } }, isNew ? '+ Crear' : '✓ Guardar')); openModal(isNew ? 'Nueva tarea' : 'Editar tarea', form, footerBtns); } // ============ VIEWS ============ function viewMasthead() { const totalSteps = 7; const steps = []; for (let i = 0; i < totalSteps; i++) { let cls = 'progress-step'; if (state.step > i) cls += ' done'; else if (state.step === i) cls += ' current'; steps.push(h('div', { class: cls })); } const hamburger = h('button', { class: 'hamburger-btn', style: 'margin-right: -4px', onclick: toggleMobileMenu, 'aria-label': 'Menú' }, '☰'); return h('header', { class: 'masthead' }, h('div', { class: 'masthead-top', style: 'display:flex;justify-content:space-between;align-items:center;width:100%' }, h('div', { class: 'brand', style: 'display:flex;align-items:center;padding:8px 0' }, h('img', { src: 'icons/mdq-titulo-horizontal.svg', alt: 'MDQ', class: 'masthead-logo' }) ), h('div', { style: 'display:flex;align-items:center;gap:12px' }, h('img', { src: 'img/logo OEQ QGob.png', alt: 'Oficina de Empleo Quilmes', class: 'desktop-only', style: 'height:44px; opacity:0.8; margin-right:12px' }), hamburger ) ), state.currentTab === 'setup' ? h('div', { class: 'progress' }, ...steps, h('span', { class: 'progress-label' }, `Paso ${Math.min(state.step + 1, totalSteps)}/${totalSteps}`) ) : null ); } let _mobileMenuOpen = false; function toggleMobileMenu() { _mobileMenuOpen = !_mobileMenuOpen; const overlay = document.getElementById('mobile-menu-overlay'); if (overlay) overlay.classList.toggle('open', _mobileMenuOpen); } function closeMobileMenu() { _mobileMenuOpen = false; const overlay = document.getElementById('mobile-menu-overlay'); if (overlay) overlay.classList.remove('open'); } function viewNav() { const tabs = [ { id: 'home', label: 'Inicio', icon: '🏠' }, { id: 'setup', label: 'Mi plan', icon: '📋' }, { id: 'today', label: 'Hoy', icon: '☀️' }, { id: 'week', label: 'Semana', icon: '📅' }, { id: 'diary', label: 'Diario', icon: '📓' }, { id: 'ideas', label: 'Ideas', icon: '💡' }, { id: 'tools', label: 'Herramientas', icon: '🧰' }, { id: 'metrics', label: 'Métricas', icon: '📊' }, { id: 'learn', label: 'Aprender', icon: '📚' } ]; const desktopNav = h('nav', { class: 'nav-tabs' }, ...tabs.map(t => h('button', { class: 'nav-tab' + (state.currentTab === t.id ? ' active' : ''), onclick: () => setTab(t.id) }, t.label)) ); const mobileOverlay = h('div', { class: 'mobile-menu-overlay' + (_mobileMenuOpen ? ' open' : ''), id: 'mobile-menu-overlay', onclick: (e) => { if (e.target.id === 'mobile-menu-overlay') closeMobileMenu(); } }, h('div', { class: 'mobile-menu' }, h('div', { class: 'mobile-menu-header' }, h('img', { src: 'icons/mdq-titulo-horizontal.svg', alt: 'MDQ', class: 'mobile-menu-logo', style: 'height:36px' }), h('button', { class: 'mobile-menu-close', onclick: closeMobileMenu }, '✕') ), h('div', { class: 'mobile-menu-nav' }, ...tabs.map(t => h('button', { class: 'mobile-menu-tab' + (state.currentTab === t.id ? ' active' : ''), onclick: () => { setTab(t.id); closeMobileMenu(); } }, t.icon + ' ' + t.label)) ), h('div', { class: 'mobile-menu-footer' }, h('img', { src: 'img/logo OEQ QGob.png', alt: 'Quilmes Gobierno' }) ) ) ); return h('div', {}, desktopNav, mobileOverlay ); } // ---------- HOME ---------- function viewHome() { const tieneSetup = state.emprendimiento.nombre && state.objetivo.tipo; return h('section', { class: 'section active' }, h('div', { class: 'hero' }, h('div', { class: 'section-kicker' }, 'MDQ · Quilmes 2026'), h('h1', { class: 'hero-title', html: 'Tu marca
con dirección
digital.' }), h('p', { class: 'hero-lead' }, 'Una herramienta pedagógica y práctica para emprendedoras y emprendedores que quieren ordenar su marketing digital. Diagnóstico, plan, cronograma y acompañamiento diario. Sin cuenta, sin costo, sin complicaciones.' ), h('div', { class: 'btn-row' }, tieneSetup ? h('button', { class: 'btn btn-primary', onclick: () => setTab('today') }, 'Ir a mi panel de hoy →') : h('button', { class: 'btn btn-primary', onclick: () => setTab('setup') }, 'Empezar mi diagnóstico →'), h('button', { class: 'btn btn-ghost', onclick: () => setTab('learn') }, 'Explorar contenidos') ), h('blockquote', { class: 'hero-quote' }, '"No se trata de estar en todas las redes ni de usar todas las herramientas. Se trata de conocer a tu cliente, elegir los canales correctos, publicar con constancia y medir para mejorar."' ), h('div', { class: 'hero-features' }, h('div', { class: 'hero-feat' }, h('div', { class: 'hero-feat-num' }, '01'), h('div', { class: 'hero-feat-title' }, 'Diagnóstico honesto'), h('div', { class: 'hero-feat-text' }, 'Un FODA guiado y una evaluación de tus aptitudes digitales, sin juzgarte. Sabés dónde estás parado.') ), h('div', { class: 'hero-feat' }, h('div', { class: 'hero-feat-num' }, '02'), h('div', { class: 'hero-feat-title' }, 'Plan personalizado'), h('div', { class: 'hero-feat-text' }, 'Modelo de negocio sugerido y cronograma semanal adaptado a tus horas reales, no a las ideales.') ), h('div', { class: 'hero-feat' }, h('div', { class: 'hero-feat-num' }, '03'), h('div', { class: 'hero-feat-title' }, 'Acompañamiento diario'), h('div', { class: 'hero-feat-text' }, 'Modo "hoy" con las tareas del día, biblioteca de ideas por rubro y exportación a Google Calendar.') ), h('div', { class: 'hero-feat' }, h('div', { class: 'hero-feat-num' }, '04'), h('div', { class: 'hero-feat-title' }, 'Aprendés haciendo'), h('div', { class: 'hero-feat-text' }, 'Microlecciones en cada paso, glosario de términos y guía de herramientas gratuitas.') ) ), h('div', { class: 'divider' }), h('div', { class: 'card' }, h('div', { class: 'pill' }, 'Cómo funciona'), h('h3', { class: 'card-title mt-1' }, 'Un recorrido en 7 pasos'), h('div', { class: 'card-body' }, h('p', {}, '1. Contanos sobre tu emprendimiento (nombre, rubro, descripción).'), h('p', {}, '2. Definí tu cliente ideal con una guía paso a paso.'), h('p', {}, '3. Hacé tu FODA (fortalezas, debilidades, oportunidades, amenazas).'), h('p', {}, '4. Autoevaluá tus aptitudes digitales y recibí microlecciones según tu nivel.'), h('p', {}, '5. Contanos sobre tus redes actuales y las que querés crecer.'), h('p', {}, '6. Indicanos cuántas horas por semana podés dedicarle realmente.'), h('p', {}, '7. Recibí tu plan completo: modelo, cronograma, métricas y herramientas.') ) ) ) ); } // ---------- SETUP WIZARD ---------- function viewSetup() { const steps = [ setupStep0, setupStep1, setupStep2, setupStep3, setupStep4, setupStep5, setupStep6, setupStep7 ]; const fn = steps[state.step] || setupStep0; return fn(); } // El botón "Siguiente" guarda referencia al último creado para poder // refrescar su estado sin re-renderizar toda la vista (y así no perder // el foco del input cuando el usuario está tipeando). let _lastNextBtn = null; let _lastCanNextFn = null; function refreshNavButton() { if (!_lastNextBtn || !_lastCanNextFn) return; const can = !!_lastCanNextFn(); _lastNextBtn._canNext = can; if (can) { _lastNextBtn.setAttribute('style', ''); } else { _lastNextBtn.setAttribute('style', 'opacity:0.4;cursor:not-allowed;pointer-events:none'); } } function setupNavButtons(canNext, onNext, backLabel = '← Volver') { // canNext puede ser booleano (compat) o función (lazy re-evaluable) const canNextFn = typeof canNext === 'function' ? canNext : () => canNext; const initialCan = !!canNextFn(); const buttons = []; if (state.step > 0) { buttons.push(h('button', { class: 'btn btn-secondary', onclick: () => setStep(state.step - 1) }, backLabel)); } const nextBtn = h('button', { class: 'btn btn-primary', onclick: () => { if (_lastCanNextFn && _lastCanNextFn()) { onNext ? onNext() : setStep(state.step + 1); } }, style: initialCan ? '' : 'opacity:0.4;cursor:not-allowed;pointer-events:none' }, 'Siguiente →'); nextBtn._canNext = initialCan; _lastNextBtn = nextBtn; _lastCanNextFn = canNextFn; buttons.push(nextBtn); return h('div', { class: 'btn-row' }, ...buttons); } // Step 0: Emprendimiento function setupStep0() { const e = state.emprendimiento; return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 1 · Tu emprendimiento'), h('h2', { class: 'section-title', html: 'Contame quién sos.' }), h('p', { class: 'section-intro' }, 'Empecemos por lo básico: el nombre de tu proyecto y qué ofrecés. Esto va a ser la base de todo lo demás.') ), h('div', { class: 'card' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Nombre del emprendimiento'), h('input', { class: 'input', type: 'text', placeholder: 'Ej: Tortas de Valeria', value: e.nombre, oninput: (ev) => { state.emprendimiento.nombre = ev.target.value; saveState(); refreshNavButton(); } }), h('div', { class: 'field-hint' }, 'Puede ser tu nombre real, un nombre de marca o lo que uses en redes.') ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Rubro principal'), (() => { const sel = h('select', { class: 'select', onchange: (ev) => { state.emprendimiento.rubro = ev.target.value; saveState(); refreshNavButton(); } }, h('option', { value: '' }, '— Elegí tu rubro —'), ...RUBROS.map(r => { const opt = h('option', { value: r }, r); if (e.rubro === r) opt.selected = true; return opt; }) ); return sel; })() ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Describí en una frase qué ofrecés'), h('textarea', { class: 'textarea', placeholder: 'Ej: Tortas artesanales por encargo para cumpleaños y eventos en zona sur.', oninput: (ev) => { state.emprendimiento.descripcion = ev.target.value; saveState(); refreshNavButton(); } }, e.descripcion), h('div', { class: 'field-hint' }, 'Si pudieras decir lo que hacés en una sola oración, ¿cuál sería? Es tu primera práctica de copywriting.') ) ), h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '📚 Lección 1'), h('h4', { class: 'lesson-title' }, 'Claridad > creatividad'), h('div', { class: 'lesson-body' }, h('p', {}, 'Más del 90% del contenido publicado en internet no recibe tráfico. ¿Por qué? Porque no es claro. Si en 5 segundos alguien que entra a tu perfil no entiende qué vendés, se va. Empezá por lo más simple: un nombre, un rubro, una frase.') ) ), setupNavButtons(() => state.emprendimiento.nombre && state.emprendimiento.rubro && state.emprendimiento.descripcion) ); } // Step 1: Buyer Persona function setupStep1() { const b = state.buyer; const campos = [ { k: 'nombre', label: 'Nombre ficticio y edad', ph: 'Ej: Valeria, 34 años' }, { k: 'situacion', label: 'Situación / ocupación', ph: 'Ej: Trabaja en oficina, tiene 2 hijos chicos' }, { k: 'necesidad', label: '¿Qué necesita que vos podés resolver?', ph: 'Ej: Comida rica pero rápida, sin cocinar mucho' }, { k: 'donde_busca', label: '¿Dónde busca información?', ph: 'Ej: Instagram, Google en el celu, grupos de Facebook' }, { k: 'frenos', label: '¿Qué la frena para comprarte?', ph: 'Ej: Desconfianza, precio, no saber si le llega' }, ]; return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 2 · Buyer Persona'), h('h2', { class: 'section-title', html: '¿A quién le hablás?' }), h('p', { class: 'section-intro' }, 'El error más común es publicar sin saber exactamente a quién. Vamos a crear a tu cliente ideal: una persona concreta, con nombre, con necesidades reales.') ), h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '💡 Consejo'), h('h4', { class: 'lesson-title' }, 'No lo inventes: observalo'), h('div', { class: 'lesson-body' }, h('p', {}, 'Si ya tenés clientes, pensá en los 2-3 que más te gustan o que más te compran. ¿Cómo son? ¿Qué te dijeron cuando te compraron? Eso es oro. Si todavía no tenés clientes, pensá en la persona ideal que te gustaría atender.') ) ), h('div', { class: 'card' }, ...campos.map(c => h('div', { class: 'field' }, h('label', { class: 'field-label' }, c.label), h('textarea', { class: 'textarea', style: 'min-height:60px', placeholder: c.ph, oninput: (ev) => { state.buyer[c.k] = ev.target.value; saveState(); refreshNavButton(); } }, b[c.k] || '') )) ), setupNavButtons(() => state.buyer.nombre && state.buyer.necesidad) ); } // Step 2: FODA function setupStep2() { const f = state.foda; function fodaCell(key, cls, titulo, letra, ph) { return h('div', { class: 'foda-cell ' + cls }, h('div', { class: 'foda-cell-label' }, letra + ' · ' + (cls === 'f' || cls === 'd' ? 'INTERNO' : 'EXTERNO')), h('div', { class: 'foda-cell-title' }, titulo), h('textarea', { placeholder: ph, oninput: (ev) => { state.foda[key] = ev.target.value; saveState(); refreshNavButton(); } }, f[key] || '') ); } return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 3 · FODA'), h('h2', { class: 'section-title', html: 'La foto honesta.' }), h('p', { class: 'section-intro' }, 'El FODA es una herramienta simple y poderosa: identificar qué tenés a favor, qué te falta, qué oportunidades ves y qué amenazas hay en el contexto. Ser honesto acá es el primer paso para mejorar.') ), h('div', { class: 'foda-grid' }, fodaCell('f', 'f', 'Fortalezas', 'F', '¿Qué hacés bien? ¿Qué te diferencia? ¿Qué recursos tenés?'), fodaCell('o', 'o', 'Oportunidades', 'O', '¿Qué tendencias podés aprovechar? ¿Qué cambios en el mercado te favorecen?'), fodaCell('d', 'd', 'Debilidades', 'D', '¿Qué te falta? ¿Qué te cuesta? ¿Qué habilidades no tenés todavía?'), fodaCell('a', 'a', 'Amenazas', 'A', '¿Qué competencia hay? ¿Qué puede complicarte (inflación, cambios en redes)?') ), h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '📚 Cómo leer tu FODA'), h('h4', { class: 'lesson-title' }, 'Cruzá los cuadrantes'), h('div', { class: 'lesson-body' }, h('p', {}, '→ Fortalezas + Oportunidades = dónde atacar primero (tu ventaja competitiva).'), h('p', {}, '→ Debilidades + Amenazas = qué tenés que proteger o mejorar urgente.'), h('p', {}, '→ Fortalezas + Amenazas = cómo defenderte usando lo que tenés.'), h('p', {}, '→ Debilidades + Oportunidades = qué aprender para no perder el momento.') ) ), setupNavButtons(() => state.foda.f && state.foda.d && state.foda.o && state.foda.a) ); } // Step 3: Aptitudes function setupStep3() { const areas = { estrategia: { label: 'Estrategia', apts: APTITUDES.filter(a => a.area === 'estrategia') }, ejecucion: { label: 'Ejecución', apts: APTITUDES.filter(a => a.area === 'ejecucion') }, gestion: { label: 'Gestión', apts: APTITUDES.filter(a => a.area === 'gestion') } }; function aptitudRow(a) { const val = state.aptitudes[a.id] || 0; const scaleEl = h('div', { class: 'aptitude-scale' }); function buildDots(currentVal) { clearNode(scaleEl); for (let i = 1; i <= 5; i++) { const dot = h('button', { class: 'aptitude-dot' + (currentVal >= i ? ' selected' : ''), 'data-apt': a.id, 'data-val': String(i) }, String(i)); dot.addEventListener('click', () => { state.aptitudes[a.id] = i; saveState(); buildDots(i); refreshNavButton(); }); scaleEl.appendChild(dot); } } buildDots(val); return h('div', { class: 'aptitude' }, h('div', { class: 'aptitude-head' }, h('div', {}, h('div', { class: 'aptitude-title' }, a.titulo), h('div', { class: 'aptitude-sub' }, a.sub) ) ), scaleEl ); } const todasRespondidas = APTITUDES.every(a => state.aptitudes[a.id] > 0); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 4 · Autodiagnóstico'), h('h2', { class: 'section-title', html: 'Tus aptitudes.' }), h('p', { class: 'section-intro' }, 'Ponete una nota del 1 al 5 en cada habilidad. 1 = no tengo idea, 5 = lo manejo muy bien. Sé honesto: esto es para vos, no para nadie más. Según tus puntajes bajos, te vamos a sugerir microlecciones.') ), ...Object.entries(areas).map(([k, area]) => h('div', { class: 'panel' }, h('div', { class: 'panel-head' }, h('span', {}, area.label.toUpperCase()), h('span', {}, `${area.apts.length} ítems`) ), h('div', { class: 'panel-body' }, ...area.apts.map(aptitudRow)) )), setupNavButtons(() => APTITUDES.every(a => state.aptitudes[a.id] > 0)) ); } // Step 4: Redes actuales function setupStep4() { const redes = ['Instagram', 'Facebook', 'TikTok', 'WhatsApp Business', 'YouTube', 'LinkedIn', 'X/Twitter']; const r = state.redes; // Container for the conditional "¿en cuáles estás?" section const actualesContainer = h('div', {}); function buildActualesSection() { clearNode(actualesContainer); if (state.redes.tiene === true) { actualesContainer.appendChild(h('div', { class: 'field' }, h('label', { class: 'field-label' }, '¿En cuáles estás actualmente?'), h('div', { class: 'chips', id: 'chips-actuales' }, ...redes.map(red => { const btn = h('button', { class: 'chip' + (state.redes.actuales.includes(red) ? ' selected' : '') }, red); btn.addEventListener('click', () => { if (state.redes.actuales.includes(red)) state.redes.actuales = state.redes.actuales.filter(x => x !== red); else state.redes.actuales.push(red); btn.classList.toggle('selected', state.redes.actuales.includes(red)); saveState(); refreshNavButton(); }); return btn; }) ) )); } } // Container for "empezás de cero" lesson const lessonContainer = h('div', {}); function buildLessonSection() { clearNode(lessonContainer); if (state.redes.tiene === false) { lessonContainer.appendChild(h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '🚀 Empezás de cero'), h('h4', { class: 'lesson-title' }, '¡Bienvenida/o! Vamos paso a paso'), h('div', { class: 'lesson-body' }, h('p', {}, 'No tener redes hoy es una ventaja: vas a arrancar bien desde el principio, sin vicios ni viejos posts que borrar. Lo primero que vas a necesitar: (1) un perfil de Instagram Business, (2) WhatsApp Business con catálogo y (3) una ficha en Google Business Profile si atendés en un lugar físico o hacés envíos locales.'), h('p', {}, 'Te vamos a guiar en cada paso. Cuando terminemos el diagnóstico, vas a tener un cronograma listo para arrancar esta misma semana.') ) )); } } buildActualesSection(); buildLessonSection(); // "¿Tenés redes?" choice buttons const btnSi = h('button', { class: 'choice' + (r.tiene === true ? ' selected' : '') }, h('span', { class: 'choice-bullet' }), h('span', {}, 'Sí, ya tengo al menos una red activa') ); const btnNo = h('button', { class: 'choice' + (r.tiene === false ? ' selected' : '') }, h('span', { class: 'choice-bullet' }), h('span', {}, 'No, todavía no empecé') ); btnSi.addEventListener('click', () => { state.redes.tiene = true; saveState(); btnSi.classList.add('selected'); btnNo.classList.remove('selected'); buildActualesSection(); buildLessonSection(); refreshNavButton(); }); btnNo.addEventListener('click', () => { state.redes.tiene = false; state.redes.actuales = []; saveState(); btnNo.classList.add('selected'); btnSi.classList.remove('selected'); buildActualesSection(); buildLessonSection(); refreshNavButton(); }); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 5 · Tus redes'), h('h2', { class: 'section-title', html: 'Tu presencia digital.' }), h('p', { class: 'section-intro' }, 'Contame si ya estás en redes o si arrancás de cero. Las dos respuestas son válidas: para cada caso hay una estrategia distinta.') ), h('div', { class: 'card' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, '¿Ya tenés presencia en alguna red social?'), h('div', { class: 'choices' }, btnSi, btnNo) ), actualesContainer, h('div', { class: 'field' }, h('label', { class: 'field-label' }, '¿En qué red te gustaría crecer (o empezar)?'), h('div', { class: 'chips' }, ...redes.map(red => { const btn = h('button', { class: 'chip' + (r.objetivo_crecer.includes(red) ? ' selected' : '') }, red); btn.addEventListener('click', () => { if (state.redes.objetivo_crecer.includes(red)) state.redes.objetivo_crecer = state.redes.objetivo_crecer.filter(x => x !== red); else state.redes.objetivo_crecer.push(red); btn.classList.toggle('selected', state.redes.objetivo_crecer.includes(red)); saveState(); refreshNavButton(); }); return btn; }) ), h('div', { class: 'field-hint' }, 'Tip: elegí máximo 2. Es mejor dominar una red que estar perdido en cinco.') ) ), lessonContainer, setupNavButtons(() => state.redes.tiene !== null && state.redes.objetivo_crecer.length > 0) ); } // Step 5: Disponibilidad function setupStep5() { const d = state.disponibilidad; // Range display element — updated in-place const rangeDisplay = h('div', { class: 'range-value', html: `${d.horas_semana}horas / semana` }); const rangeInput = h('input', { type: 'range', min: '1', max: '20', value: String(d.horas_semana) }); rangeInput.addEventListener('input', (ev) => { const val = parseInt(ev.target.value); state.disponibilidad.horas_semana = val; rangeDisplay.innerHTML = `${val}horas / semana`; saveState(); }); // Day chips const dayChips = DIAS.map(dia => { const btn = h('button', { class: 'chip' + (d.dias_activos.includes(dia) ? ' selected' : '') }, dia); btn.addEventListener('click', () => { if (state.disponibilidad.dias_activos.includes(dia)) state.disponibilidad.dias_activos = state.disponibilidad.dias_activos.filter(x => x !== dia); else state.disponibilidad.dias_activos.push(dia); btn.classList.toggle('selected', state.disponibilidad.dias_activos.includes(dia)); saveState(); refreshNavButton(); }); return btn; }); // Franja choices const franjaOpts = [ ['mañana', 'Mañana', '07:00 - 12:00'], ['tarde', 'Tarde', '13:00 - 18:00'], ['noche', 'Noche', '19:00 - 23:00'] ]; const franjaButtons = franjaOpts.map(([val, label, hs]) => { const btn = h('button', { class: 'choice' + (d.franja === val ? ' selected' : '') }, h('span', { class: 'choice-bullet' }), h('span', {}, h('strong', {}, label), ' · ', hs) ); btn.addEventListener('click', () => { state.disponibilidad.franja = val; saveState(); franjaButtons.forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); }); return btn; }); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 6 · Tu tiempo real'), h('h2', { class: 'section-title', html: 'Cuánto tiempo tenés.' }), h('p', { class: 'section-intro' }, 'Sé realista, no ideal. Es mejor un plan de 3 horas por semana que cumplís, que uno de 10 que abandonás al segundo mes. La constancia vence a la intensidad.') ), h('div', { class: 'card' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Horas por semana que podés dedicarle'), h('div', { class: 'range-wrap' }, rangeDisplay, rangeInput) ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Días de la semana que podés trabajar el marketing'), h('div', { class: 'chips' }, ...dayChips), h('div', { class: 'field-hint' }, 'Elegí al menos 2 días. Podés dejar de seleccionar si necesitás descansar algún día específico.') ), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Franja horaria preferida'), h('div', { class: 'choices' }, ...franjaButtons) ) ), h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '⏱ Realidad'), h('h4', { class: 'lesson-title' }, 'Con pocas horas, se puede'), h('div', { class: 'lesson-body' }, h('p', {}, 'Con 3 horas por semana ya podés tener presencia consistente: 1 hora para crear el contenido de la semana, 1 hora distribuida en subirlo y responder, 1 hora para medir resultados. No se trata de cantidad, se trata de hacerlo siempre.') ) ), setupNavButtons(() => state.disponibilidad.dias_activos.length >= 2) ); } // Step 6: Objetivo SMART function setupStep6() { const o = state.objetivo; const tipos = [ { v: 'visibilidad', l: 'Más visibilidad de marca', d: 'Que más gente me conozca en mi zona' }, { v: 'engagement', l: 'Más interacción y comunidad', d: 'Que la gente participe, comente, comparta' }, { v: 'ventas', l: 'Más consultas y ventas', d: 'Que más gente me escriba para comprar' }, { v: 'fidelizacion', l: 'Fidelizar clientes que ya tengo', d: 'Que quienes ya compraron vuelvan a comprar' } ]; function armarSmart(tipo, numero, plazo) { if (!tipo || !numero) return ''; const mapTexto = { visibilidad: `Llegar a ${numero} personas nuevas por semana a través de mis redes, en los próximos ${plazo}.`, engagement: `Conseguir ${numero} interacciones reales (comentarios, guardados, compartidos) por semana, en los próximos ${plazo}.`, ventas: `Generar ${numero} consultas o ventas por mes a través de WhatsApp e Instagram, en los próximos ${plazo}.`, fidelizacion: `Lograr que al menos ${numero}% de mis clientes vuelvan a comprar por mes, en los próximos ${plazo}.` }; return mapTexto[tipo] || ''; } // Containers that show/hide based on tipo selection const metaContainer = h('div', {}); const smartPreviewContainer = h('div', {}); // Plazo chips — built once, kept alive const plazos = ['1 mes', '3 meses', '6 meses']; const plazoButtons = plazos.map(p => { const btn = h('button', { class: 'chip' + (o.meta_plazo === p ? ' selected' : '') }, p); btn.addEventListener('click', () => { state.objetivo.meta_plazo = p; saveState(); plazoButtons.forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); updateSmartPreview(); refreshNavButton(); }); return btn; }); // Number input — built once const hintMap = { visibilidad: 'Cantidad de personas nuevas alcanzadas por semana.', engagement: 'Interacciones reales (comentarios, guardados, compartidos) por semana.', ventas: 'Consultas o ventas por mes.', fidelizacion: 'Porcentaje de recompra mensual (un 30% es muy bueno).' }; const phMap = { visibilidad: 'Ej: 500', ventas: 'Ej: 20', engagement: 'Ej: 50', fidelizacion: 'Ej: 30' }; const fieldHintEl = h('div', { class: 'field-hint' }, hintMap[o.tipo] || ''); const numInput = h('input', { class: 'input', type: 'number', placeholder: phMap[o.tipo] || 'Ej: 50', value: o.meta_numero || '' }); numInput.addEventListener('input', (ev) => { state.objetivo.meta_numero = ev.target.value; saveState(); updateSmartPreview(); refreshNavButton(); }); const metaField = h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Meta concreta (un número)'), numInput, fieldHintEl ); const plazoField = h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Plazo'), h('div', { class: 'chips' }, ...plazoButtons) ); function updateMetaContainer() { clearNode(metaContainer); if (state.objetivo.tipo) { // Update hints/placeholder based on selected tipo fieldHintEl.textContent = hintMap[state.objetivo.tipo] || ''; numInput.placeholder = phMap[state.objetivo.tipo] || 'Ej: 50'; metaContainer.appendChild(metaField); metaContainer.appendChild(plazoField); } } function updateSmartPreview() { clearNode(smartPreviewContainer); const smart = armarSmart(state.objetivo.tipo, state.objetivo.meta_numero, state.objetivo.meta_plazo); state.objetivo.texto_smart = smart; saveState(); if (smart) { smartPreviewContainer.appendChild(h('div', { class: 'lesson', style: 'margin-top:24px' }, h('div', { class: 'lesson-tag' }, '✓ Tu objetivo'), h('h4', { class: 'lesson-title' }, 'Así se lee tu meta SMART:'), h('div', { class: 'lesson-body' }, h('p', { style: 'font-family:var(--serif);font-size:17px;font-style:italic' }, '"' + smart + '"') ) )); } } // Tipo choice buttons const tipoButtons = tipos.map(t => { const btn = h('button', { class: 'choice' + (o.tipo === t.v ? ' selected' : '') }, h('span', { class: 'choice-bullet' }), h('span', {}, h('strong', {}, t.l), h('br'), h('span', { style: 'font-size:12px;opacity:0.8' }, t.d)) ); btn.addEventListener('click', () => { state.objetivo.tipo = t.v; saveState(); tipoButtons.forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); updateMetaContainer(); updateSmartPreview(); refreshNavButton(); }); return btn; }); // Initialize containers based on existing state updateMetaContainer(); updateSmartPreview(); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Paso 7 · Tu objetivo SMART'), h('h2', { class: 'section-title', html: 'Un objetivo concreto.' }), h('p', { class: 'section-intro' }, 'No sirve "quiero vender más". Sirve "quiero X cantidad de Y en Z tiempo". Vamos a armar tu objetivo SMART juntos.') ), h('div', { class: 'card' }, h('div', { class: 'field' }, h('label', { class: 'field-label' }, '¿Qué es lo más importante para vos ahora?'), h('div', { class: 'choices' }, ...tipoButtons) ), metaContainer, smartPreviewContainer ), setupNavButtons(() => !!(state.objetivo.tipo && state.objetivo.meta_numero)) ); } // Step 7: Resultados + plan function setupStep7() { const scores = calcularScores(); const bajas = aptitudesBajas(); const modelo = sugerirModeloNegocio(); if (!state.cronograma_base || JSON.stringify(state.cronograma_base) === 'null') { state.cronograma_base = generarCronograma(); state.cronograma = state.cronograma_base; // legacy compat state.fecha_inicio = state.fecha_inicio || new Date().toISOString(); saveState(); } function scoreBar(label, value) { const pct = (value / 5) * 100; const lvl = value >= 4 ? 'high' : value >= 2.5 ? 'mid' : 'low'; return h('div', { class: 'score-item' }, h('div', { class: 'score-bar-wrap' }, h('div', { class: 'score-label' }, h('span', {}, label), h('span', {}, value.toFixed(1) + ' / 5') ), h('div', { class: 'score-bar' }, h('div', { class: 'score-bar-fill level-' + lvl, style: `width:${pct}%` }) ) ) ); } return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, '🎉 Diagnóstico completo'), h('h2', { class: 'section-title', html: 'Tu plan está listo.' }), h('p', { class: 'section-intro' }, 'Armamos tu estrategia personalizada a partir de tus respuestas. Explorá los resultados y usá las pestañas de arriba para tu día a día.') ), h('div', { class: 'panel' }, h('div', { class: 'panel-head' }, h('span', {}, 'TUS APTITUDES DIGITALES'), h('span', {}, `Promedio ${scores.total.toFixed(1)}/5`)), h('div', { class: 'panel-body' }, h('div', { class: 'score-grid' }, scoreBar('Estrategia', scores.estrategia), scoreBar('Ejecución', scores.ejecucion), scoreBar('Gestión', scores.gestion) ) ) ), h('div', { class: 'panel' }, h('div', { class: 'panel-head' }, h('span', {}, 'MODELO DE NEGOCIO SUGERIDO')), h('div', { class: 'panel-body' }, h('p', { style: 'font-size:15px;line-height:1.55;margin-bottom:14px' }, modelo.descripcion), h('div', { style: 'display:grid;gap:10px;font-size:13px' }, h('div', {}, h('strong', { style: 'font-family:var(--mono);text-transform:uppercase;font-size:10px;color:var(--terra)' }, 'Canal principal: '), modelo.canal_principal), h('div', {}, h('strong', { style: 'font-family:var(--mono);text-transform:uppercase;font-size:10px;color:var(--terra)' }, 'Canal de cierre: '), modelo.canal_cierre), h('div', {}, h('strong', { style: 'font-family:var(--mono);text-transform:uppercase;font-size:10px;color:var(--terra)' }, 'Cobro: '), modelo.cobro), h('div', {}, h('strong', { style: 'font-family:var(--mono);text-transform:uppercase;font-size:10px;color:var(--terra)' }, 'Ritmo: '), modelo.ritmo), modelo.escalabilidad ? h('div', {}, h('strong', { style: 'font-family:var(--mono);text-transform:uppercase;font-size:10px;color:var(--terra)' }, 'Próximos pasos: '), modelo.escalabilidad) : null ) ) ), h('div', { class: 'card' }, h('h3', { class: 'card-title' }, 'Lo que sigue'), h('div', { class: 'card-body' }, h('p', {}, 'Tu cronograma semanal está listo en la pestaña ', h('strong', {}, 'Semana'), ', las tareas de hoy en ', h('strong', {}, 'Hoy'), ', y tenés ideas de contenido para tu rubro en ', h('strong', {}, 'Ideas'), '.'), h('p', {}, 'Podés cambiar tu modalidad de ritmo de trabajo desde la pestaña ', h('strong', {}, 'Semana'), '.') ), h('div', { class: 'btn-row' }, h('button', { class: 'btn btn-primary', onclick: () => setTab('today') }, 'Ver mi día de hoy →'), h('button', { class: 'btn btn-secondary', onclick: () => setTab('week') }, 'Ver cronograma semanal') ) ), // Microlecciones MOVED BEFORE DRIVE & NOTIFS bajas.length > 0 ? h('div', { style: 'margin-top:24px' }, h('h3', { class: 'card-title' }, '📚 Tus microlecciones personalizadas'), h('p', { style: 'font-size:14px;color:var(--ink-soft);margin-bottom:16px' }, `Según tu autodiagnóstico, identificamos ${bajas.length} aptitud${bajas.length > 1 ? 'es' : ''} que podés mejorar:`), ...bajas.map(a => { const lesson = LESSONS[a.id]; if (!lesson) return null; return h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '📚 ' + a.titulo), h('h4', { class: 'lesson-title' }, lesson.titulo), h('div', { class: 'lesson-body' }, h('p', {}, lesson.texto)) ); }) ) : null, // Notification preferences card viewNotifPrefs(), // Google Drive sync card viewDriveSync() ); } // ============ NOTIFICATIONS (v2.4: browser Notification API) ============ let _notifTimers = []; function notifSupported() { return 'Notification' in window; } function notifPermitted() { return notifSupported() && Notification.permission === 'granted'; } async function requestNotifPermission() { if (!notifSupported()) return false; if (Notification.permission === 'granted') return true; if (Notification.permission === 'denied') return false; const result = await Notification.requestPermission(); return result === 'granted'; } function getNotifPrefs() { return state.notif_prefs || { habilitadas: false, recordatorio_tarea: true, inicio_dia: true, cierre_dia: true, racha: true }; } function setNotifPrefs(prefs) { state.notif_prefs = prefs; saveState(); programarNotificacionesDiarias(); } function showNotif(title, body, tag) { if (!notifPermitted()) return; try { const n = new Notification(title, { body: body, icon: './icons/icon-192.png', badge: './icons/icon-192.png', tag: tag || 'brujula-' + Date.now(), silent: false }); n.onclick = () => { window.focus(); n.close(); }; } catch (e) { console.warn('[Notif] Error:', e); } } function iconoCategoria(cat) { const m = { creacion: '\u270d\ufe0f', publicacion: '\ud83d\udce4', medicion: '\ud83d\udcca', planificacion: '\ud83d\udcdd', gestion: '\ud83d\udce9', aprendizaje: '\ud83d\udcda' }; return m[cat] || '\u2726'; } function programarNotificacionesDiarias() { // Clear all existing timers _notifTimers.forEach(t => clearTimeout(t)); _notifTimers = []; const prefs = getNotifPrefs(); if (!prefs.habilitadas || !notifPermitted()) return; const cronograma = getCronogramaEfectivo(); if (!cronograma) return; const ahora = new Date(); const hoy = new Date(ahora); const diaIdx = (hoy.getDay() + 6) % 7; const diaNombre = DIAS[diaIdx]; const tareasHoy = (cronograma[diaNombre] || []).filter(t => t.tipo !== 'rest'); // --- Task reminders (15min before) --- if (prefs.recordatorio_tarea && tareasHoy.length > 0) { tareasHoy.forEach(t => { if (!t.hora) return; const [hh, mm] = t.hora.split(':').map(Number); const tareaTime = new Date(hoy); tareaTime.setHours(hh, mm, 0, 0); const reminderTime = new Date(tareaTime.getTime() - 15 * 60 * 1000); // 15min before const delay = reminderTime.getTime() - ahora.getTime(); if (delay > 0 && delay < 24 * 60 * 60 * 1000) { _notifTimers.push(setTimeout(() => { showNotif( iconoCategoria(t.categoria_actividad) + ' En 15 min: ' + t.titulo, t.detalle || 'Prepárate para tu próxima tarea.', 'task-' + t.id ); }, delay)); } }); } // --- Daily start notification --- if (prefs.inicio_dia && tareasHoy.length > 0) { const franja = state.disponibilidad?.franja || 'mañana'; const startHour = franja === 'mañana' ? 8 : franja === 'tarde' ? 13 : 18; const startTime = new Date(hoy); startTime.setHours(startHour, 0, 0, 0); const delay = startTime.getTime() - ahora.getTime(); if (delay > 0 && delay < 24 * 60 * 60 * 1000) { _notifTimers.push(setTimeout(() => { const nombre = state.emprendimiento?.nombre || ''; showNotif( '\u2600\ufe0f \u00a1Buen d\u00eda' + (nombre ? ', ' + nombre : '') + '!', 'Hoy ten\u00e9s ' + tareasHoy.length + ' tarea' + (tareasHoy.length > 1 ? 's' : '') + ': ' + tareasHoy.map(t => t.titulo).join(', ') + '.', 'start-day' ); }, delay)); } } // --- Close day notification --- if (prefs.cierre_dia) { const closeTime = new Date(hoy); closeTime.setHours(21, 0, 0, 0); const delay = closeTime.getTime() - ahora.getTime(); if (delay > 0 && delay < 24 * 60 * 60 * 1000) { _notifTimers.push(setTimeout(() => { const fechaHoy = getFechaISO(hoy); const dayTracking = state.tracking[fechaHoy] || {}; const hechas = tareasHoy.filter(t => dayTracking[t.id] && dayTracking[t.id].hecha).length; const pendientes = tareasHoy.length - hechas; showNotif( '\ud83c\udf19 \u00bfC\u00f3mo te fue hoy?', pendientes > 0 ? 'Te quedan ' + pendientes + ' tarea' + (pendientes > 1 ? 's' : '') + ' pendiente' + (pendientes > 1 ? 's' : '') + '. Marc\u00e1 lo que hiciste y dej\u00e1 una nota.' : '\u00a1Completaste todo! Dej\u00e1 un resumen del d\u00eda.', 'close-day' ); }, delay)); } } // --- Streak notification (immediate check after completing a task) --- if (prefs.racha) { const racha = getRachas(); const lastNotifiedStreak = parseInt(sessionStorage.getItem('_lastStreakNotif') || '0'); if (racha >= 3 && racha > lastNotifiedStreak && racha % 1 === 0) { // Only notify on milestones: 3, 5, 7, 10, 14, 21, 30... const milestones = [3, 5, 7, 10, 14, 21, 30, 50, 100]; if (milestones.includes(racha)) { showNotif( '\ud83d\udd25 \u00a1' + racha + ' d\u00edas consecutivos!', '\u00a1Incre\u00edble constancia! Segu\u00ed as\u00ed, cada d\u00eda cuenta.', 'streak-' + racha ); sessionStorage.setItem('_lastStreakNotif', String(racha)); } } } console.log('[Notif] Programadas ' + _notifTimers.length + ' notificaciones para hoy'); } // Notification preferences UI (rendered inside Mi Plan / settings area) function viewNotifPrefs() { const prefs = getNotifPrefs(); const supported = notifSupported(); const permitted = notifPermitted(); function toggle(key) { prefs[key] = !prefs[key]; setNotifPrefs(prefs); render(); } async function toggleMaster() { if (!prefs.habilitadas) { const ok = await requestNotifPermission(); if (ok) { prefs.habilitadas = true; setNotifPrefs(prefs); render(); } } else { prefs.habilitadas = false; setNotifPrefs(prefs); render(); } } if (!supported) { return h('div', { class: 'card', style: 'opacity:.6' }, h('h3', { class: 'card-title' }, '\ud83d\udd14 Notificaciones'), h('div', { class: 'card-body' }, h('p', {}, 'Tu navegador no soporta notificaciones. Prob\u00e1 abriendo la app en Chrome o Firefox.') ) ); } return h('div', { class: 'card' }, h('h3', { class: 'card-title' }, '\ud83d\udd14 Notificaciones'), h('div', { class: 'card-body' }, h('p', {}, 'Recib\u00ed recordatorios mientras ten\u00e9s la app abierta. Para notificaciones de fondo, export\u00e1 el cronograma a Google Calendar.'), Notification.permission === 'denied' ? h('p', { style: 'color:var(--terra);font-weight:600' }, '\u26a0\ufe0f Las notificaciones est\u00e1n bloqueadas en tu navegador. And\u00e1 a la configuraci\u00f3n del sitio para habilitarlas.') : null, h('div', { class: 'notif-toggle-row' }, h('label', { class: 'notif-toggle' }, h('span', {}, 'Activar notificaciones'), h('div', { class: 'toggle-switch' + (prefs.habilitadas && permitted ? ' on' : ''), onclick: toggleMaster }, h('div', { class: 'toggle-knob' }) ) ) ), prefs.habilitadas && permitted ? h('div', { class: 'notif-options' }, ...[ { key: 'recordatorio_tarea', label: '\u23f0 Recordatorio 15min antes de cada tarea' }, { key: 'inicio_dia', label: '\u2600\ufe0f Resumen del d\u00eda al iniciar la jornada' }, { key: 'cierre_dia', label: '\ud83c\udf19 Recordatorio para cerrar el d\u00eda (21:00)' }, { key: 'racha', label: '\ud83d\udd25 Celebraci\u00f3n de rachas' } ].map(opt => h('label', { class: 'notif-option' }, h('input', { type: 'checkbox', checked: prefs[opt.key], onchange: () => toggle(opt.key) }), h('span', {}, opt.label) )) ) : null ) ); } // ============ GOOGLE DRIVE SYNC (v2.5) ============ const GDRIVE_CLIENT_ID = '967525128930-kp63e6d9qj094ppdblm59989p3m10cur.apps.googleusercontent.com'; // ← PONER TU CLIENT ID DE GOOGLE CLOUD CONSOLE const GDRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.appdata'; const GDRIVE_FILE_NAME = 'brujula_digital_state.json'; let _gdriveToken = null; let _gdriveSyncTimer = null; let _gdriveSyncStatus = 'idle'; // idle | syncing | synced | error | offline let _gdriveLastSyncTime = null; function gdriveAvailable() { return typeof google !== 'undefined' && google.accounts && GDRIVE_CLIENT_ID; } function gdriveSignIn() { if (!gdriveAvailable()) { alert('Google Drive no está disponible. Verificá que tengas conexión a internet y que el Client ID esté configurado.'); return; } const client = google.accounts.oauth2.initTokenClient({ client_id: GDRIVE_CLIENT_ID, scope: GDRIVE_SCOPE, callback: (tokenResponse) => { if (tokenResponse.error) { console.warn('[GDrive] Auth error:', tokenResponse.error); _gdriveSyncStatus = 'error'; render(); return; } _gdriveToken = tokenResponse.access_token; sessionStorage.setItem('_gdriveToken', _gdriveToken); state.gdrive = state.gdrive || {}; state.gdrive.enabled = true; saveState(); gdrivePull().then(() => render()); } }); client.requestAccessToken(); } function gdriveSignOut() { if (_gdriveToken) { try { google.accounts.oauth2.revoke(_gdriveToken); } catch (e) { } } _gdriveToken = null; sessionStorage.removeItem('_gdriveToken'); _gdriveSyncStatus = 'idle'; if (state.gdrive) state.gdrive.enabled = false; saveState(); render(); } async function gdriveApiCall(url, options = {}) { if (!_gdriveToken) throw new Error('No auth token'); const res = await fetch(url, { ...options, headers: { Authorization: 'Bearer ' + _gdriveToken, ...(options.headers || {}) } }); if (res.status === 401) { _gdriveToken = null; sessionStorage.removeItem('_gdriveToken'); _gdriveSyncStatus = 'error'; throw new Error('Token expired'); } return res; } async function gdriveFindFile() { const res = await gdriveApiCall( 'https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&q=name%3D%27' + GDRIVE_FILE_NAME + '%27&fields=files(id,modifiedTime)' ); const data = await res.json(); return data.files && data.files.length > 0 ? data.files[0] : null; } async function gdriveUpload(stateJson) { const existing = await gdriveFindFile(); const metadata = { name: GDRIVE_FILE_NAME, mimeType: 'application/json' }; if (!existing) metadata.parents = ['appDataFolder']; const boundary = '===brujula_boundary==='; const body = '--' + boundary + '\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n' + JSON.stringify(metadata) + '\r\n--' + boundary + '\r\nContent-Type: application/json\r\n\r\n' + stateJson + '\r\n--' + boundary + '--'; const url = existing ? 'https://www.googleapis.com/upload/drive/v3/files/' + existing.id + '?uploadType=multipart' : 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'; const res = await gdriveApiCall(url, { method: existing ? 'PATCH' : 'POST', headers: { 'Content-Type': 'multipart/related; boundary=' + boundary }, body: body }); return res.json(); } async function gdriveDownload() { const file = await gdriveFindFile(); if (!file) return null; const res = await gdriveApiCall( 'https://www.googleapis.com/drive/v3/files/' + file.id + '?alt=media' ); return { data: await res.json(), modifiedTime: file.modifiedTime }; } async function gdrivePush() { if (!_gdriveToken) return; try { _gdriveSyncStatus = 'syncing'; const stateJson = JSON.stringify(state); await gdriveUpload(stateJson); state.gdrive = state.gdrive || {}; state.gdrive.lastSyncAt = new Date().toISOString(); state.gdrive.lastSyncHash = simpleHash(stateJson); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); _gdriveSyncStatus = 'synced'; _gdriveLastSyncTime = new Date(); console.log('[GDrive] Push OK'); } catch (e) { console.warn('[GDrive] Push failed:', e); _gdriveSyncStatus = navigator.onLine ? 'error' : 'offline'; } } async function gdrivePull() { if (!_gdriveToken) return; try { _gdriveSyncStatus = 'syncing'; const remote = await gdriveDownload(); if (!remote) { // No remote file yet — push current state await gdrivePush(); return; } const localUpdated = state.updated_at ? new Date(state.updated_at) : new Date(0); const remoteUpdated = remote.data.updated_at ? new Date(remote.data.updated_at) : new Date(remote.modifiedTime); if (remoteUpdated > localUpdated) { // Remote is newer — load it console.log('[GDrive] Remote is newer, loading...'); const remoteState = remote.data; if (remoteState.version) { Object.assign(state, remoteState); state.gdrive = state.gdrive || {}; state.gdrive.enabled = true; state.gdrive.lastSyncAt = new Date().toISOString(); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); _gdriveSyncStatus = 'synced'; _gdriveLastSyncTime = new Date(); } } else { // Local is newer or same — push await gdrivePush(); } } catch (e) { console.warn('[GDrive] Pull failed:', e); _gdriveSyncStatus = navigator.onLine ? 'error' : 'offline'; } } function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return hash.toString(36); } function debouncedDriveSync() { if (!_gdriveToken || !state.gdrive?.enabled) return; if (_gdriveSyncTimer) clearTimeout(_gdriveSyncTimer); _gdriveSyncStatus = 'pending'; _gdriveSyncTimer = setTimeout(() => gdrivePush(), 5000); } // Restore token from session on reload function gdriveRestoreSession() { const token = sessionStorage.getItem('_gdriveToken'); if (token && state.gdrive?.enabled) { _gdriveToken = token; gdrivePull().then(() => render()); } } // Monitor online/offline window.addEventListener('online', () => { if (_gdriveSyncStatus === 'offline' && _gdriveToken) { gdrivePush().then(() => render()); } }); window.addEventListener('offline', () => { if (_gdriveToken) _gdriveSyncStatus = 'offline'; }); function getSyncStatusText() { switch (_gdriveSyncStatus) { case 'synced': { if (!_gdriveLastSyncTime) return '✅ Sincronizado'; const ago = Math.round((Date.now() - _gdriveLastSyncTime.getTime()) / 1000); if (ago < 60) return '✅ Hace ' + ago + 's'; if (ago < 3600) return '✅ Hace ' + Math.round(ago / 60) + ' min'; return '✅ Sincronizado'; } case 'syncing': return '🔄 Sincronizando...'; case 'pending': return '⏳ Guardando...'; case 'error': return '⚠️ Error de sync'; case 'offline': return '🔌 Offline'; default: return ''; } } function viewDriveSync() { const available = gdriveAvailable(); const connected = !!_gdriveToken && state.gdrive?.enabled; if (!GDRIVE_CLIENT_ID) { return h('div', { class: 'card', style: 'opacity: .5' }, h('h3', { class: 'card-title' }, '🔁 Sincronización con Google Drive'), h('div', { class: 'card-body' }, h('p', {}, 'La sincronización con Google Drive requiere configurar un Client ID en Google Cloud Console. Consultá la documentación para más info.') ) ); } return h('div', { class: 'card' }, h('h3', { class: 'card-title' }, '🔁 Sincronización con Google Drive'), h('div', { class: 'card-body' }, !connected ? h('div', {}, h('p', {}, 'Sincronizá tu progreso con Google Drive para acceder desde cualquier dispositivo. Tus datos quedan en TU Drive — nadie más los lee.'), h('p', { style: 'font-size:12px;color:var(--ink-soft)' }, 'Se usa la carpeta privada "appData" de tu Drive.'), available ? h('button', { class: 'btn btn-primary', style: 'margin-top:12px', onclick: gdriveSignIn }, '🔐 Conectar Google Drive') : h('p', { style: 'color:var(--terra)' }, 'Google Identity Services no cargó. Verificá tu conexión.') ) : h('div', {}, h('div', { style: 'display:flex;align-items:center;gap:12px;margin-bottom:12px' }, h('span', { class: 'sync-status ' + _gdriveSyncStatus }, getSyncStatusText()) ), state.gdrive?.lastSyncAt ? h('p', { class: 'sync-info' }, 'Última sincronización: ' + new Date(state.gdrive.lastSyncAt).toLocaleString('es-AR')) : null, h('div', { class: 'btn-row', style: 'margin-top:12px' }, h('button', { class: 'btn btn-secondary', onclick: () => gdrivePush().then(() => render()) }, '⬆️ Subir ahora'), h('button', { class: 'btn btn-secondary', onclick: () => gdrivePull().then(() => render()) }, '⬇️ Descargar'), h('button', { class: 'btn', style: 'color:var(--terra)', onclick: gdriveSignOut }, 'Desconectar') ) ) ) ); } // ---------- TODAY VIEW (v2: interactive checklist + tracking) ---------- function viewToday() { const cronograma = getCronogramaEfectivo(); if (!cronograma) return viewSetupFirst('today'); const hoy = new Date(); const fechaHoy = getFechaISO(hoy); const diaIdx = (hoy.getDay() + 6) % 7; const diaNombre = DIAS[diaIdx]; const tareasHoy = cronograma[diaNombre] || []; const fechaStr = hoy.toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long' }); const tracking = getNotasDe(fechaHoy); const racha = getRachas(); // Yesterday card const ayer = new Date(hoy); ayer.setDate(ayer.getDate() - 1); const fechaAyer = getFechaISO(ayer); const trackingAyer = getNotasDe(fechaAyer); const notasAyer = Object.entries(trackingAyer).filter(([k, v]) => k !== 'resumen_dia' && v && (v.nota || v.hecha)); let yesterdayOpen = false; const yesterdayBody = h('div', { class: 'yesterday-body', style: 'display:none' }); const yesterdayToggle = h('span', { class: 'yesterday-toggle' }, '\u25bc'); const yesterdayCard = notasAyer.length > 0 ? h('div', { class: 'yesterday-card', onclick: () => { yesterdayOpen = !yesterdayOpen; yesterdayBody.style.display = yesterdayOpen ? 'block' : 'none'; yesterdayToggle.classList.toggle('open', yesterdayOpen); } }, h('div', { class: 'yesterday-header' }, h('span', { class: 'yesterday-label' }, '\ud83d\udcc5 Lo que hice ayer'), yesterdayToggle ), yesterdayBody ) : null; if (yesterdayCard) { const diaAyerIdx = (ayer.getDay() + 6) % 7; const tareasAyer = cronograma[DIAS[diaAyerIdx]] || []; notasAyer.forEach(([tid, data]) => { const t = tareasAyer.find(x => x.id === tid); yesterdayBody.appendChild(h('div', { class: 'yesterday-note' }, h('strong', {}, (t ? t.titulo : 'Tarea') + (data.hecha ? ' \u2714' : '')), data.nota ? h('div', { style: 'margin-top:4px' }, data.nota) : null )); }); if (trackingAyer.resumen_dia) { yesterdayBody.appendChild(h('div', { class: 'yesterday-note', style: 'border-left-color:var(--sage)' }, h('strong', {}, 'Resumen del d\u00eda'), h('div', { style: 'margin-top:4px' }, trackingAyer.resumen_dia) )); } } function buildCheckItem(t) { const data = tracking[t.id] || { hecha: false, hora_completada: '', nota: '' }; const isDone = !!data.hecha; let noteVisible = false; const noteArea = h('div', { class: 'check-note-area', style: 'display:none' }, h('textarea', { placeholder: '\u00bfQu\u00e9 hiciste? \u00bfAlgo para recordar?', oninput: e => guardarNotaTarea(fechaHoy, t.id, e.target.value) }, data.nota || '') ); const noteBtn = h('button', { class: 'check-note-toggle', onclick: e => { e.stopPropagation(); noteVisible = !noteVisible; noteArea.style.display = noteVisible ? 'block' : 'none'; } }, data.nota ? '\ud83d\udcdd Ver nota' : '\ud83d\udcdd Anotar'); return h('div', { class: 'check-item' + (isDone ? ' done' : '') }, h('div', { class: 'check-box' + (isDone ? ' checked' : ''), onclick: e => { e.stopPropagation(); marcarTareaHecha(fechaHoy, t.id); render(); } }, isDone ? '\u2714' : ''), h('div', { class: 'check-content' }, h('div', { class: 'check-title' }, t.titulo), h('div', { class: 'check-meta' }, h('span', { class: 'cat-dot ' + (t.categoria_actividad || '') }), t.hora ? h('span', {}, t.hora) : null, t.duracion_min ? h('span', {}, t.duracion_min + ' min') : null, isDone && data.hora_completada ? h('span', { class: 'check-completed-time' }, '\u2714 ' + data.hora_completada) : null ), h('div', { class: 'check-detail' }, t.detalle), noteBtn, noteArea ) ); } const isRest = tareasHoy.length === 0 || (tareasHoy.length === 1 && tareasHoy[0].tipo === 'rest'); const tareasActivas = tareasHoy.filter(t => t.tipo !== 'rest'); const hechas = tareasActivas.filter(t => tracking[t.id] && tracking[t.id].hecha).length; return h('section', { class: 'section active' }, yesterdayCard, h('div', { class: 'today-card' }, h('div', { class: 'today-label' }, 'Tu agenda de hoy'), h('div', { class: 'today-date' }, fechaStr, racha > 1 ? h('span', { class: 'streak-badge', style: 'margin-left:12px;vertical-align:middle' }, '\ud83d\udd25 ' + racha + ' d\u00edas') : null ), !isRest ? h('div', { style: 'font-family:var(--mono);font-size:11px;color:var(--mustard);text-transform:uppercase;margin-top:8px;position:relative' }, hechas + '/' + tareasActivas.length + ' tareas completadas') : null ), isRest ? h('div', { class: 'card' }, h('h3', { class: 'card-title' }, 'D\u00eda de descanso \ud83c\udf3f'), h('div', { class: 'card-body' }, h('p', {}, 'Hoy no hay tareas agendadas. Aprovech\u00e1 para descansar.'))) : h('div', {}, ...tareasActivas.map(buildCheckItem)), !isRest ? h('div', { class: 'close-day' }, h('div', { class: 'close-day-title' }, '\ud83c\udf19 Cerrar el d\u00eda'), h('textarea', { placeholder: '\u00bfC\u00f3mo te fue hoy? Una l\u00ednea basta...', oninput: e => guardarResumenDia(fechaHoy, e.target.value) }, tracking.resumen_dia || ''), h('div', { style: 'margin-top:8px;font-size:12px;opacity:.7' }, 'Opcional. Una nota para tu yo de ma\u00f1ana.') ) : null, h('div', { class: 'btn-row', style: 'margin-top:20px' }, h('button', { class: 'btn btn-secondary btn-sm', onclick: () => setTab('week') }, 'Ver semana'), h('button', { class: 'btn btn-secondary btn-sm', onclick: () => setTab('diary') }, 'Ver diario'), h('button', { class: 'btn btn-secondary btn-sm', onclick: () => setTab('ideas') }, 'Ideas') ) ); } function viewSetupFirst(targetTab) { return h('section', { class: 'section active' }, h('div', { class: 'empty' }, h('div', { class: 'empty-icon' }, '\u2726'), h('h3', { class: 'section-title', style: 'font-size:32px' }, 'Todav\u00eda no empezaste'), h('p', { class: 'empty-text' }, 'Para ver tu plan necesitamos conocer tu emprendimiento primero.'), h('div', { class: 'mt-3' }, h('button', { class: 'btn btn-primary', onclick: () => setTab('setup') }, 'Empezar diagn\u00f3stico \u2192') ) ) ); } // ---------- WEEK VIEW (v2: clickable + editable) ---------- function viewWeek() { const cronograma = getCronogramaEfectivo(); if (!cronograma) return viewSetupFirst('week'); const hasOverrides = (state.cronograma_overrides || []).length > 0; // Schedule variant selector (moved from setup) const variantes = generarVariantes(); const variantCards = variantes.map(v => { const isSelected = state.cronograma_variante === v.nombre; const totalTareas = Object.values(v.cronograma).reduce((s, arr) => s + arr.filter(t => t.tipo !== 'rest').length, 0); const card = h('div', { class: 'card', style: `cursor:pointer;border:2px solid ${isSelected ? 'var(--terra)' : 'var(--line)'};${isSelected ? 'background:rgba(25,162,85,.06)' : ''}; transition:all .2s`, onclick: () => { state.cronograma_variante = v.nombre; state.cronograma_base = v.cronograma; state.cronograma = v.cronograma; state.cronograma_overrides = []; saveState(); render(); } }, h('div', { style: 'text-align:center;margin-bottom:12px' }, h('div', { style: 'font-size:32px' }, v.emoji), h('h3', { class: 'card-title', style: 'margin-top:8px;font-size:16px' }, v.nombre) ), h('div', { style: 'font-size:13px;color:var(--ink-soft);text-align:center;margin-bottom:12px' }, v.desc), h('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px;text-align:center' }, h('div', { style: 'background:var(--cream);padding:8px;border-radius:var(--radius-sm)' }, h('div', { style: 'font-weight:700;font-size:18px;color:var(--terra)' }, String(v.nPosts)), h('div', {}, 'posts/sem') ), h('div', { style: 'background:var(--cream);padding:8px;border-radius:var(--radius-sm)' }, h('div', { style: 'font-weight:700;font-size:18px;color:var(--violeta)' }, String(totalTareas)), h('div', {}, 'tareas') ) ), isSelected ? h('div', { style: 'text-align:center;margin-top:12px;color:var(--terra);font-weight:700;font-size:13px' }, '✓ Activo') : null ); return card; }); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Tu cronograma'), h('h2', { class: 'section-title', html: 'La semana que viene.' }), h('p', { class: 'section-intro' }, 'Hacé clic en cualquier tarea para editarla. Podés agregar, mover o eliminar tareas.') ), h('div', { style: 'display:flex;gap:14px;margin-bottom:16px;flex-wrap:wrap;font-size:11px;font-family:var(--mono)' }, ...CATEGORIAS.map(c => h('span', { style: 'display:flex;align-items:center;gap:4px' }, h('span', { class: 'cat-dot ' + c.id }), c.label)) ), h('div', { class: 'week-wrap' }, h('div', { class: 'week-grid' }, ...DIAS.map((dia, idx) => { const tareas = cronograma[dia] || []; return h('div', { class: 'day-cell' }, h('div', { class: 'day-name' }, DIAS_ABR[idx]), ...tareas.map(t => { const ov = t._overridden || isTaskOverridden(t.id); const catCls = t.categoria_actividad ? ' cat-' + t.categoria_actividad : ''; const cls = 'day-task ' + t.tipo + (t.tipo !== 'rest' ? ' clickable' : '') + (ov ? ' overridden' : '') + catCls; const card = h('div', { class: cls }, t.hora ? h('strong', {}, t.hora + (ov ? ' ✏️' : '')) : null, h('span', {}, t.titulo) ); if (t.tipo !== 'rest') card.addEventListener('click', () => openTaskEditor(t, false)); return card; }), h('button', { class: 'add-task-btn', onclick: () => openTaskEditor({ dia, hora: '', categoria_actividad: 'planificacion' }, true) }, '+ Agregar') ); }) ) ), // Variant selector — "¿Qué cronograma se ajusta mejor a vos?" h('div', { class: 'card', style: 'margin-top:24px' }, h('h3', { class: 'card-title' }, '⚙️ ¿Qué ritmo se ajusta mejor a vos?'), h('div', { class: 'card-body' }, h('p', {}, 'Elegí el ritmo que mejor se adapte a tu disponibilidad. Al seleccionar uno, el cronograma se regenera automáticamente.')), h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-top:16px' }, ...variantCards ) ), // Manage schedule h('div', { class: 'card' }, h('h3', { class: 'card-title' }, '🔧 Gestionar cronograma'), h('div', { class: 'card-body' }, h('p', {}, 'Regenerar el cronograma base o restaurar todo al original.')), h('div', { class: 'btn-row' }, h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { if (confirm('\u00bfRegenerar base? Tus ediciones se mantienen.')) { state.cronograma_base = generarCronograma(); saveState(); render(); } } }, '\ud83d\udd04 Regenerar base'), hasOverrides ? h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { if (confirm('\u00bfRestaurar todo al original?')) { resetAllOverrides(); render(); } } }, '\u21a9 Restaurar todo') : null ) ), // Notification preferences viewNotifPrefs(), // Export buttons at the end h('div', { class: 'divider' }), h('div', { class: 'btn-row' }, h('button', { class: 'btn btn-primary', onclick: descargarICS }, h('span', {}, '📅'), h('span', {}, 'Exportar a Google Calendar')), h('button', { class: 'btn btn-secondary', onclick: descargarResumenPDF }, h('span', {}, '📄'), h('span', {}, 'Descargar resumen PDF')) ) ); } // ---------- DIARY VIEW (v2: monthly calendar + day detail) ---------- let _diaryMonth = new Date().getMonth(); let _diaryYear = new Date().getFullYear(); let _diarySelectedDay = null; function viewDiario() { const cronograma = getCronogramaEfectivo(); if (!cronograma) return viewSetupFirst('diary'); const hoy = new Date(); const hoyISO = getFechaISO(hoy); const monthNames = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; const firstDay = new Date(_diaryYear, _diaryMonth, 1); const lastDay = new Date(_diaryYear, _diaryMonth + 1, 0); const startDow = (firstDay.getDay() + 6) % 7; const gridContainer = h('div', { class: 'diary-grid' }, ...DIAS_ABR.map(d => h('div', { class: 'diary-day-header' }, d)) ); for (let i = 0; i < startDow; i++) gridContainer.appendChild(h('div', { class: 'diary-day empty' })); const detailContainer = h('div', {}); function showDayDetail(dayNum) { _diarySelectedDay = dayNum; clearNode(detailContainer); const date = new Date(_diaryYear, _diaryMonth, dayNum); const dateISO = getFechaISO(date); const dIdx = (date.getDay() + 6) % 7; const tareasDia = (cronograma[DIAS[dIdx]] || []).filter(t => t.tipo !== 'rest'); const dayTracking = getNotasDe(dateISO); const dateStr = date.toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); detailContainer.appendChild(h('div', { class: 'diary-detail' }, h('div', { class: 'diary-detail-date' }, dateStr), ...tareasDia.map(t => { const td = dayTracking[t.id] || {}; return h('div', { class: 'diary-task-item' + (td.hecha ? ' completed' : '') }, h('div', { class: 'diary-task-status' }, td.hecha ? '\u2705' : '\u2b1c'), h('div', { class: 'diary-task-info' }, h('div', { class: 'diary-task-title' }, h('span', { class: 'cat-dot ' + (t.categoria_actividad || '') }), (t.hora ? t.hora + ' \u00b7 ' : '') + t.titulo), td.hora_completada ? h('div', { style: 'font-size:11px;color:var(--sage)' }, 'Completada ' + td.hora_completada) : null, td.nota ? h('div', { class: 'diary-task-note' }, td.nota) : null ) ); }), tareasDia.length === 0 ? h('div', { style: 'color:var(--ink-soft);font-style:italic;padding:12px 0' }, '\ud83c\udf3f D\u00eda de descanso') : null, dayTracking.resumen_dia ? h('div', { class: 'diary-task-note', style: 'margin-top:12px;border-left-color:var(--sage)' }, h('strong', {}, 'Resumen: '), dayTracking.resumen_dia) : null )); gridContainer.querySelectorAll('.diary-day').forEach(el => el.classList.remove('selected')); const dayEl = gridContainer.querySelector('[data-day="' + dayNum + '"]'); if (dayEl) dayEl.classList.add('selected'); } for (let d = 1; d <= lastDay.getDate(); d++) { const date = new Date(_diaryYear, _diaryMonth, d); const dateISO = getFechaISO(date); const isToday = dateISO === hoyISO; const dayTracking = state.tracking[dateISO] || {}; const dIdx = (date.getDay() + 6) % 7; const tareasDia = (cronograma[DIAS[dIdx]] || []).filter(t => t.tipo !== 'rest'); let dotClass = ''; if (tareasDia.length > 0) { const completed = tareasDia.filter(t => dayTracking[t.id] && dayTracking[t.id].hecha).length; dotClass = completed === 0 ? 'none' : completed >= tareasDia.length ? 'complete' : 'partial'; } const dayNum = d; gridContainer.appendChild(h('div', { class: 'diary-day' + (isToday ? ' today' : '') + (_diarySelectedDay === d ? ' selected' : ''), 'data-day': String(d), onclick: () => showDayDetail(dayNum) }, h('span', {}, String(d)), dotClass ? h('div', { class: 'diary-dot ' + dotClass }) : null)); } if (_diarySelectedDay === null && _diaryMonth === hoy.getMonth() && _diaryYear === hoy.getFullYear()) { setTimeout(() => showDayDetail(hoy.getDate()), 0); } else if (_diarySelectedDay) { setTimeout(() => showDayDetail(_diarySelectedDay), 0); } return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Tu diario'), h('h2', { class: 'section-title', html: 'D\u00eda a d\u00eda.' }), h('p', { class: 'section-intro' }, 'Tu registro de actividad. Puntos verdes = 100% cumplido, amarillos = parcial. Clic en un d\u00eda para ver detalle.') ), h('div', { class: 'diary-nav' }, h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { _diaryMonth--; if (_diaryMonth < 0) { _diaryMonth = 11; _diaryYear--; } _diarySelectedDay = null; render(); } }, '\u2190'), h('div', { class: 'diary-month' }, monthNames[_diaryMonth] + ' ' + _diaryYear), h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { _diaryMonth++; if (_diaryMonth > 11) { _diaryMonth = 0; _diaryYear++; } _diarySelectedDay = null; render(); } }, '\u2192') ), h('div', { style: 'display:flex;gap:14px;margin-bottom:12px;font-size:11px;font-family:var(--mono)' }, h('span', { style: 'display:flex;align-items:center;gap:4px' }, h('span', { class: 'diary-dot complete', style: 'position:static' }), 'Completo'), h('span', { style: 'display:flex;align-items:center;gap:4px' }, h('span', { class: 'diary-dot partial', style: 'position:static' }), 'Parcial'), h('span', { style: 'display:flex;align-items:center;gap:4px' }, h('span', { class: 'diary-dot none', style: 'position:static' }), 'Pendiente') ), gridContainer, detailContainer, getRachas() > 1 ? h('div', { class: 'card', style: 'text-align:center;margin-top:16px' }, h('div', { class: 'streak-badge', style: 'font-size:14px' }, '\ud83d\udd25 Racha de ' + getRachas() + ' d\u00edas'), h('p', { style: 'margin-top:12px;font-size:14px;color:var(--ink-soft)' }, '\u00a1Segu\u00ed as\u00ed! La constancia es tu mejor herramienta.') ) : null ); } // ---------- IDEAS VIEW ---------- let _ideasOpen = { util: false, emocional: false, comercial: false }; function viewIdeas() { const rubro = state.emprendimiento.rubro || 'tu rubro'; const nombre = state.emprendimiento.nombre || 'tu marca'; function personalizar(s) { return s .replace(/{RUBRO}/g, rubro.toLowerCase()) .replace(/{NOMBRE}/g, nombre) .replace(/{PRODUCTO}/g, 'tu producto') .replace(/{TEMA_RUBRO}/g, rubro.toLowerCase()) .replace(/{OFERTA}/g, 'tu oferta') .replace(/{OCASIÓN}/g, 'la próxima fecha especial') .replace(/{PRODUCTO_A}/g, 'producto A') .replace(/{PRODUCTO_B}/g, 'producto B'); } function makeAccordion(key, label, pct, ideas, typeClass) { const headEl = h('div', { class: 'panel-head' }, h('span', {}, label), h('span', { style: 'display:flex;align-items:center;gap:8px' }, h('span', {}, pct), h('span', { class: 'panel-toggle' + (_ideasOpen[key] ? '' : ' collapsed') }, '▾') ) ); const bodyEl = h('div', { class: 'panel-body' + (_ideasOpen[key] ? '' : ' collapsed') }, ...ideas.map(i => h('div', { class: 'idea' }, h('span', { class: 'idea-type ' + typeClass }, typeClass === 'util' ? 'ÚTIL' : typeClass === 'emo' ? 'EMOCIONAL' : 'COMERCIAL'), h('div', { class: 'idea-title' }, personalizar(i.title)), h('div', { class: 'idea-format' }, i.format) )) ); headEl.onclick = () => { _ideasOpen[key] = !_ideasOpen[key]; headEl.querySelector('.panel-toggle').classList.toggle('collapsed', !_ideasOpen[key]); bodyEl.classList.toggle('collapsed', !_ideasOpen[key]); }; return h('div', { class: 'panel ideas-panel' }, headEl, bodyEl); } return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Biblioteca de ideas'), h('h2', { class: 'section-title', html: 'Ideas de contenido.' }), h('p', { class: 'section-intro' }, 'Cuando estés frente a tu celular sin saber qué publicar, volvé acá. Tocá cada categoría para ver ideas personalizadas.') ), h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '📐 Regla 60/40'), h('h4', { class: 'lesson-title' }, 'Balance para que no te dejen de seguir'), h('div', { class: 'lesson-body' }, h('p', {}, '60% útil o emocional + 40% comercial. Si solo vendés, aburrís. Si solo aportás valor, no generás ingresos. El equilibrio es la clave.') ) ), makeAccordion('util', '🎯 ÚTIL · Resolvé un problema', '60%', IDEAS_POOL.util, 'util'), makeAccordion('emocional', '❤ EMOCIONAL · Conectá con tu historia', '60%', IDEAS_POOL.emocional, 'emo'), makeAccordion('comercial', '💰 COMERCIAL · Vendé directo', '40%', IDEAS_POOL.comercial, 'com'), h('div', { class: 'card' }, h('h3', { class: 'card-title' }, 'Pedile más ideas a la IA'), h('div', { class: 'card-body' }, h('p', {}, 'Copiá este prompt y pegalo en ChatGPT, Claude o Gemini:'), h('div', { style: 'background:var(--ink);color:var(--mustard);padding:14px;font-family:var(--mono);font-size:12px;line-height:1.6;margin-top:12px;white-space:pre-wrap;border-radius:var(--radius-sm)' }, `Soy ${nombre}, tengo un emprendimiento de ${rubro} en Quilmes, Buenos Aires. Dame 10 ideas de contenido para Instagram adaptadas a mi cliente ideal [describí acá a tu buyer persona]. Que sean: 3 útiles, 3 emocionales, 4 comerciales. Tono cercano, argentino, sin clichés.` ) ) ) ); } // ---------- TOOLS VIEW ---------- function viewTools() { const cats = [...new Set(TOOLS.map(t => t.cat))]; return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Herramientas'), h('h2', { class: 'section-title', html: 'Tu kit gratuito.' }), h('p', { class: 'section-intro' }, 'Todas las herramientas que necesitás para empezar, organizadas por necesidad. La mayoría tiene plan gratuito más que suficiente para los primeros meses.') ), ...cats.map(cat => h('div', {}, h('h3', { class: 'card-title mt-3', style: 'font-size:18px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.08em;border-bottom:2px solid var(--ink);padding-bottom:8px;margin-bottom:16px' }, cat), h('div', { class: 'tools-grid' }, ...TOOLS.filter(t => t.cat === cat).map(t => h('div', { class: 'tool' }, h('div', { class: 'tool-name' }, t.name), h('div', { class: 'tool-desc' }, t.desc), h('span', { class: 'tool-tag' + (t.tag === 'Freemium' ? ' paid' : '') }, t.tag), h('br'), h('a', { class: 'tool-link', href: t.url, target: '_blank', rel: 'noopener' }, t.url.replace('https://', '') + ' ↗') )) ) )) ); } // ---------- METRICS VIEW ---------- let _metricFormOpen = false; let _metricPlat = 'Instagram'; let _metricV1 = ''; let _metricV2 = ''; let _metricV3 = ''; function viewMetrics() { if (!state.objetivo.tipo) return viewSetupFirst('metrics'); const kpisPorObjetivo = { visibilidad: [ { id: 'alcance', name: 'Alcance semanal', unit: 'personas', hint: 'Personas únicas que vieron tu contenido.' }, { id: 'impresiones', name: 'Impresiones', unit: 'vistas', hint: 'Cuántas veces se mostró tu contenido.' }, { id: 'seguidores', name: 'Nuevos seguidores', unit: 'netos', hint: 'Nuevos menos los que dejaron de seguirte.' } ], engagement: [ { id: 'interaccion', name: 'Interacciones', unit: 'acciones', hint: 'Comentarios + guardados + compartidos.' }, { id: 'guardados', name: 'Guardados', unit: 'totales', hint: 'La métrica estrella. Indica valor real.' }, { id: 'comentarios', name: 'Comentarios', unit: 'promedio', hint: 'Comentarios genuinos por post.' } ], ventas: [ { id: 'consultas', name: 'Consultas', unit: 'mensajes', hint: 'Personas que escribieron con intención de compra.' }, { id: 'conversion', name: 'Conversión', unit: '% compra', hint: 'De cada 10 consultas, cuántas compran.' }, { id: 'ticket', name: 'Ticket promedio', unit: 'ARS', hint: 'El valor promedio de cada venta.' } ], fidelizacion: [ { id: 'recompra', name: 'Tasa recompra', unit: '%', hint: 'Qué porcentaje volvió a comprar este mes.' }, { id: 'referidos', name: 'Referidos', unit: 'clientes', hint: 'Nuevos que llegaron por recomendación.' }, { id: 'reseñas', name: 'Reseñas', unit: 'positivas', hint: 'Crecimiento en Google o Facebook.' } ] }; const kpis = kpisPorObjetivo[state.objetivo.tipo] || []; if (!state.metricas_semanales) state.metricas_semanales = []; const history = state.metricas_semanales; let plataformas = Array.from(new Set([...(state.redes.actuales || []), ...(state.redes.objetivo_crecer || [])])); if (plataformas.length === 0) plataformas = ['Instagram', 'WhatsApp', 'Facebook']; if (!plataformas.includes(_metricPlat)) _metricPlat = plataformas[0]; function saveWeeklyMetrics() { state.metricas_semanales.unshift({ id: Date.now(), fecha: new Date().toISOString(), plataforma: _metricPlat, vals: [Number(_metricV1) || 0, Number(_metricV2) || 0, Number(_metricV3) || 0] }); saveState(); _metricFormOpen = false; _metricV1 = ''; _metricV2 = ''; _metricV3 = ''; render(); } function getTrend(current, previous) { if (previous == null) return ''; if (current > previous) return h('span', { class: 'metrics-trend up' }, '↑'); if (current < previous) return h('span', { class: 'metrics-trend down' }, '↓'); return h('span', { class: 'metrics-trend same' }, '−'); } // Find previous week's record for the same platform function getPrevRecord(currentIdx, plat) { for (let i = currentIdx + 1; i < history.length; i++) { if (history[i].plataforma === plat) return history[i]; } return null; } const formEl = _metricFormOpen ? h('div', { class: 'card mb-3' }, h('h3', { class: 'card-title' }, 'Registrar nueva semana'), h('div', { class: 'field' }, h('label', { class: 'field-label' }, 'Plataforma'), h('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, ...plataformas.map(p => h('button', { class: 'platform-chip' + (_metricPlat === p ? ' active' : ''), onclick: () => { _metricPlat = p; render(); } }, p)) ) ), h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:12px;margin-bottom:16px' }, h('div', {}, h('div', { style: 'font-size:11px;font-weight:600;margin-bottom:4px;color:var(--ink-soft)' }, kpis[0].name), h('input', { class: 'input metric-input-sm', type: 'number', placeholder: kpis[0].unit, value: _metricV1, oninput: (e) => _metricV1 = e.target.value })), h('div', {}, h('div', { style: 'font-size:11px;font-weight:600;margin-bottom:4px;color:var(--ink-soft)' }, kpis[1].name), h('input', { class: 'input metric-input-sm', type: 'number', placeholder: kpis[1].unit, value: _metricV2, oninput: (e) => _metricV2 = e.target.value })), h('div', {}, h('div', { style: 'font-size:11px;font-weight:600;margin-bottom:4px;color:var(--ink-soft)' }, kpis[2].name), h('input', { class: 'input metric-input-sm', type: 'number', placeholder: kpis[2].unit, value: _metricV3, oninput: (e) => _metricV3 = e.target.value })) ), h('div', { class: 'btn-row' }, h('button', { class: 'btn btn-primary btn-sm', onclick: saveWeeklyMetrics }, 'Guardar métricas'), h('button', { class: 'btn btn-ghost btn-sm', onclick: () => { _metricFormOpen = false; render(); } }, 'Cancelar') ) ) : h('button', { class: 'btn btn-primary', style: 'width:100%;margin-bottom:20px', onclick: () => { _metricFormOpen = true; render(); } }, '+ Registrar métricas de esta semana'); const historyEl = history.length === 0 ? h('div', { class: 'card' }, h('div', { style: 'text-align:center;color:var(--ink-soft);padding:20px 0' }, 'Todavía no hay registros semanales. ¡Registrá tu primera semana!') ) : h('div', { class: 'card', style: 'padding:0' }, h('div', { class: 'metrics-table-wrap' }, h('table', { class: 'metrics-table' }, h('thead', {}, h('tr', {}, h('th', {}, 'Fecha'), h('th', {}, 'Plataforma'), h('th', { style: 'text-align:right' }, kpis[0].name), h('th', { style: 'text-align:right' }, kpis[1].name), h('th', { style: 'text-align:right' }, kpis[2].name) ) ), h('tbody', {}, ...history.map((record, idx) => { const dateStr = new Date(record.fecha).toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }); const prev = getPrevRecord(idx, record.plataforma); return h('tr', {}, h('td', {}, dateStr), h('td', {}, h('span', { class: 'platform-chip' }, record.plataforma)), h('td', { style: 'text-align:right' }, record.vals[0], getTrend(record.vals[0], prev ? prev.vals[0] : null)), h('td', { style: 'text-align:right' }, record.vals[1], getTrend(record.vals[1], prev ? prev.vals[1] : null)), h('td', { style: 'text-align:right' }, record.vals[2], getTrend(record.vals[2], prev ? prev.vals[2] : null)) ); }) ) ) ) ); return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Tu tablero de seguimiento'), h('h2', { class: 'section-title', html: 'Historial semanal.' }), h('p', { class: 'section-intro' }, 'Registrá tus números cada semana. El sistema multi-plataforma te permite trackear Instagram, WhatsApp u otros canales por separado para ver la evolución real.') ), h('div', { class: 'lesson mb-3' }, h('div', { class: 'lesson-tag' }, '🎯 Objetivo: ' + state.objetivo.tipo.toUpperCase()), h('h4', { class: 'lesson-title' }, state.objetivo.texto_smart || 'Objetivo SMART'), h('div', { class: 'lesson-body' }, h('p', {}, 'Tus KPIs clave son: ' + kpis.map(k => k.name).join(', ') + '.')) ), formEl, historyEl, h('div', { class: 'card mt-3' }, h('h3', { class: 'card-title' }, 'El ciclo de mejora continua'), h('div', { class: 'card-body' }, h('p', {}, '1. ', h('strong', {}, 'Planificar:'), ' qué vas a publicar esta semana.'), h('p', {}, '2. ', h('strong', {}, 'Ejecutar:'), ' publicar, responder, atender.'), h('p', {}, '3. ', h('strong', {}, 'Medir:'), ' anotar estos KPIs los viernes.'), h('p', {}, '4. ', h('strong', {}, 'Aprender:'), ' ¿qué funcionó? ¿qué no?'), h('p', {}, '5. ', h('strong', {}, 'Ajustar:'), ' hacer más de lo que funciona.'), h('p', {}, '6. ', h('strong', {}, 'Repetir:'), ' volver al paso 1 con lo aprendido.') ) ) ); } // ---------- LEARN VIEW ---------- function viewLearn() { return h('section', { class: 'section active' }, h('div', { class: 'section-head' }, h('div', { class: 'section-kicker' }, 'Aprender'), h('h2', { class: 'section-title', html: 'Conceptos clave.' }), h('p', { class: 'section-intro' }, 'Lecciones cortas y glosario de términos. Volvé cuando dudes sobre una palabra o quieras profundizar un tema.') ), h('h3', { class: 'card-title', style: 'font-size:22px;margin-top:16px' }, '📚 Todas las microlecciones'), ...Object.entries(LESSONS).map(([k, v]) => h('div', { class: 'lesson' }, h('div', { class: 'lesson-tag' }, '📚 ' + (APTITUDES.find(a => a.id === k)?.titulo || k)), h('h4', { class: 'lesson-title' }, v.titulo), h('div', { class: 'lesson-body' }, h('p', {}, v.texto)) )), h('div', { class: 'divider' }), h('h3', { class: 'card-title', style: 'font-size:22px' }, '📖 Glosario de términos'), h('p', { style: 'font-size:14px;color:var(--ink-soft);margin-bottom:16px' }, 'Los términos que vas a escuchar seguido en el mundo del marketing digital.'), h('div', { class: 'card' }, ...GLOSSARY.map(([term, def]) => h('div', { class: 'gloss-item' }, h('div', { class: 'gloss-term' }, term), h('div', { class: 'gloss-def' }, def) )) ), h('div', { class: 'card mt-3' }, h('h3', { class: 'card-title' }, 'Para seguir aprendiendo'), h('div', { class: 'card-body' }, h('p', {}, '→ ', h('strong', {}, 'Google Activate:'), ' cursos gratuitos de marketing digital.'), h('p', {}, '→ ', h('strong', {}, 'Meta Blueprint:'), ' formación oficial de Instagram y Facebook para negocios.'), h('p', {}, '→ ', h('strong', {}, 'HubSpot Academy:'), ' cursos con certificación en español.'), h('p', {}, '→ ', h('strong', {}, 'Metricool Blog:'), ' artículos actualizados sobre algoritmos.') ) ) ); } // ============ RENDER ============ function render() { const app = document.getElementById('app'); clearNode(app); app.appendChild(viewMasthead()); app.appendChild(viewNav()); let view; switch (state.currentTab) { case 'home': view = viewHome(); break; case 'setup': view = viewSetup(); break; case 'today': view = viewToday(); break; case 'week': view = viewWeek(); break; case 'diary': view = viewDiario(); break; case 'ideas': view = viewIdeas(); break; case 'tools': view = viewTools(); break; case 'metrics': view = viewMetrics(); break; case 'learn': view = viewLearn(); break; default: view = viewHome(); } app.appendChild(view); // Footer const foot = h('footer', { class: 'foot' }, h('div', { style: 'margin-bottom:8px' }, h('img', { src: 'img/logo OEQ QGob.png', alt: 'Oficina de Empleo · Quilmes Gobierno', class: 'footer-logo' })), h('div', {}, '✦ MDQ · Taller de Marketing Digital 2026'), h('div', {}, 'Hecho con amor para emprendedoras y emprendedores locales'), h('div', { style: 'margin-top:12px' }, h('button', { class: 'btn btn-ghost btn-sm', onclick: resetState }, 'Borrar mis datos') ) ); app.appendChild(foot); } // ============ BOOT ============ render(); programarNotificacionesDiarias(); gdriveRestoreSession(); // ============ PWA SERVICE WORKER (Bypassed for testing) ============ if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistrations().then(function (registrations) { for (let registration of registrations) { registration.unregister(); } }); }