🏠

Casita

Nuestro hogar en Narvarte

Casita 🏠

Dashboard del hogar
0 SALUD
🆕 Novedades
💰 Presupuesto por m² — Estimado de Remodelación
Basado en precios promedio CDMX 2026. Costos de remodelación por espacio.
Espacio $/m² Total

📅 Timeline

Timeline Estimado
Ordenado por tu uso
General
💰 Finanzas
📋 Progreso
🏠 Depa
⚖️ Legal
🔧 Tools
🎨 Inspiración
🎯 Ruta
💰 Cotizador
📍 Barrio
📦 Inventario
📞 Comunicación
Propiedad
📐 Distribución por espacio
📊 Tabla de Áreas
# Espacio Área (m²) Tipo
✍️
¡Escrituras firmadas!
El depa es suyo. Ahora: entrega de llaves y preparar la mudanza.
✅ COMPLETADO Trámite BBVA — click para expandir
📄 Documentos
Precio total
🧮 Simulador Financiero
Abrir en nueva pestaña ↗
📊 Escenarios

✅ Checklist

📆 Calendario

📅 Calendario de Deadlines
⏰ Deadlines críticos

🏢 Edificio

🏢 Reglamento del Edificio

    📸 Fotos

    📸 Galería del Departamento

    Fotos del depto 402 vacío — marzo 2026. Click para ampliar.

    📐 Planos

    📐 Plano — Depto 402
    📐 Editar

    Plano interactivo — usa los filtros de capas dentro del visor

    📋 DATOS DEL PROYECTO
    🚪 Especificaciones de Puertas y Ventanas
    # Tipo Medida (m) Material
    📝 Notas de Remodelación

      📦 Inventario

      📦 Inventario de Mudanza
      🚚 Se lleva 0
        💰 Se vende 0
          🗑️ Se tira/dona 0

            📞 Comunicación

            📞 Log de Comunicaciones

            🔧 Remodelación

            🔨 Presupuesto de Remodelación
            Concepto Estimado Real Status
            TOTAL $0 $0
            $0
            Presupuesto estimado
            $0
            Gasto real
            $0
            Diferencia
            💾 Backup & Restaurar

            Exporta o importa todos tus datos de Casita (dashboard, distribución, editor de cuartos, moodboard y más).

            🎨 Mood Board — Inspiración para el depa

            Agrega imágenes de Pinterest, fotos, paletas de colores para cada espacio

            📷
            Arrastra imágenes aquí, haz click para subir, o pega una URL
            🎯 Tu siguiente paso
            Cargando...
            🎯 Ruta Crítica — ¿Qué sigue?

            Cada paso depende de los anteriores. No puedes completar un paso si sus dependencias no están listas.

            💰 Cotizador de Remodelación — CDMX 2026

            Precios promedio CDMX 2026. Selecciona conceptos, calidad y cantidad para estimar tu presupuesto.

            🏠 Desglose por Cuarto

            Estimado automático basado en m² del plano (si disponible).

            📦 Inventario de Mudanza
            📞 Log de Comunicación
            const $=s=>document.querySelector(s); const $$=s=>document.querySelectorAll(s); // ═══ DATA LAYER ═══ const FALLBACK_DATA={"meta":{"actualizado":"12 abril 2026"},"propiedad":{"direccion":"Xola 1360, Depto 402 · Narvarte Poniente · CDMX","direccionCompleta":"Xola 1360, Narvarte Poniente, Benito Juárez, CP 03020, CDMX","precio":4290000,"precioDisplay":"$4.29M","superficie":"128 m²","recamaras":3,"estacionamientos":2,"predial":"Pagado","valorCatastral":"$1.47M","proyecto":"Departamento 402 — Xola 1360","escala":"1:60","fecha":"Abril 2026","propietario":"Pame & Art"},"kpis":[{"valor":"2/7","label":"Pasos BBVA","sub":"29% completado","color":"green","estilo":"var(--green)"},{"valor":"~6","label":"Semanas p/ firma","sub":"~mayo 2026","color":"amber","estilo":"var(--amber)"},{"valor":"$900K","label":"Faltante","sub":"del enganche","color":"red","estilo":"var(--red)"},{"valor":"3","label":"Riesgos activos","sub":"1 alto, 2 medio","color":"amber","estilo":"var(--orange)"}],"digest":{"icono":"📋","texto":"Estás en el paso 3 de 7 del flujo BBVA. Próxima acción: integración de expediente. Riesgos activos: 3"},"timelineSummary":"📅 Timeline — Paso 3 de 7 · ~6 semanas para firma","flujo_bbva":[{"paso":1,"nombre":"Promesa\nfirmada","status":"done"},{"paso":2,"nombre":"Carta\ninstrucción","status":"done"},{"paso":3,"nombre":"Integración\nexpediente","status":"current"},{"paso":4,"nombre":"Envío\na DANF","status":"pending"},{"paso":5,"nombre":"VoBo","status":"pending"},{"paso":6,"nombre":"Firma\n~mayo","status":"pending"},{"paso":7,"nombre":"Entrega\n~junio","status":"pending"}],"riesgos":[{"icono":"💰","titulo":"Faltante $900K","desc":"Definir fuente de fondos para completar enganche","nivel":"high"},{"icono":"🅿️","titulo":"Estacionamientos","desc":"Cajones 15/16 en batería — mapa vs escritura incierto","nivel":"medium"},{"icono":"⚖️","titulo":"Boleta Jornada","desc":"Pendiente de firma en Notaría 211","nivel":"medium"},{"icono":"📋","titulo":"Reglamento","desc":"4 puntos legalmente cuestionables","nivel":"medium"}],"finanzas":{"precioTotal":4290000,"credito":{"monto":3000000,"porcentaje":70,"color":"var(--green)"},"ahorro":{"monto":700000,"porcentaje":16,"color":"var(--amber)"},"faltante":{"monto":900000,"porcentaje":21,"color":"var(--red)"},"predial":{"monto":9520,"bimestres":6,"catastral":1474715,"fiscal":4924000},"escenarios":[{"mensual":30000,"id":"esc-30k"},{"mensual":50000,"id":"esc-50k"},{"mensual":75000,"id":"esc-75k"},{"mensual":100000,"id":"esc-100k"}],"documentos":[{"icono":"🏛️","nombre":"Predial 2026","tipo":"PDF","href":"docs/pdfs/predial-2026.pdf"}]},"timeline":[{"fecha":"Abril 2026","titulo":"Promesa firmada + Carta de instrucción","desc":"Notaría 211, jornada notarial activada","status":"done"},{"fecha":"Abril – Mayo 2026","titulo":"Integración expediente + DANF","desc":"Middle office pendiente de asignación","status":"current"},{"fecha":"Mayo 2026","titulo":"VoBo + Preparación firma","desc":"Validación jurídica final","status":"future"},{"fecha":"Mayo – Junio 2026","titulo":"Firma de escrituras","desc":"4-6 semanas desde promesa","status":"future"},{"fecha":"Junio 2026","titulo":"Entrega de llaves","desc":"6-8 semanas desde promesa","status":"future"},{"fecha":"Junio – Julio 2026","titulo":"Preparación del depa","desc":"Limpieza, pintura, verificar estacionamientos","status":"future"},{"fecha":"Julio 2026","titulo":"¡Mudanza! 🏠","desc":"Horario: 9am-6pm (reglamento del edificio)","status":"future"}],"calendario":[{"mes":"Abril 2026","current":true,"icono":"🟢","eventos":[{"texto":"Promesa firmada","status":"done"},{"texto":"Carta instrucción","status":"done"},{"texto":"⚠️ Boleta jornada notarial","status":"active"},{"texto":"Integración expediente","status":"active"}]},{"mes":"Mayo 2026","current":false,"eventos":[{"texto":"Envío a DANF","status":"future"},{"texto":"VoBo","status":"future"},{"texto":"📝 Firma escrituras (est.)","status":"future"}]},{"mes":"Junio 2026","current":false,"eventos":[{"texto":"🔑 Entrega de llaves (est.)","status":"future"},{"texto":"Inspección del depa","status":"future"},{"texto":"Cotizar mudanzas","status":"future"}]},{"mes":"Julio 2026","current":false,"eventos":[{"texto":"Contratar internet","status":"future"},{"texto":"Verificar servicios","status":"future"},{"texto":"📦 ¡Mudanza!","status":"future"}]}],"deadlines":[{"icono":"⚠️","texto":"Boleta jornada notarial","estado":"PENDIENTE","color":"var(--red)"},{"icono":"💰","texto":"Cubrir $900K faltante","estado":"Antes de firma","color":"var(--orange)"},{"icono":"🅿️","texto":"Verificar estacionamientos","estado":"Antes de firma","color":"var(--orange)"},{"icono":"📦","texto":"Aviso mudanza al edificio","estado":"24h antes","color":"var(--text-dim)"}],"checklist":{"grupos":[{"titulo":"Legal / Financiero","id":"ck1","items":[{"key":"promesa","label":"Promesa firmada"},{"key":"carta","label":"Carta de instrucción emitida"},{"key":"avaluo","label":"Avalúo realizado"},{"key":"anticipo","label":"Anticipo notaría pagado"},{"key":"boleta","label":"Boleta jornada notarial firmada"},{"key":"faltante","label":"Cubrir $900K faltante"},{"key":"firma","label":"Firma de escrituras"},{"key":"estac","label":"Verificar estacionamientos en escritura"},{"key":"llaves","label":"Entrega de llaves"}]},{"titulo":"Mudanza","id":"ck2","items":[{"key":"cotizar","label":"Cotizar 3+ servicios de mudanza"},{"key":"inventario","label":"Inventario de lo que se lleva"},{"key":"avisoviejo","label":"Avisar al edificio actual (24h antes)"},{"key":"avisonuevo","label":"Avisar al edificio nuevo (24h antes)"},{"key":"empacar","label":"Empacar por cuartos"}]},{"titulo":"Servicios","id":"ck3","items":[{"key":"internet","label":"Contratar internet"},{"key":"gas","label":"Verificar gas"},{"key":"agua","label":"Verificar agua"},{"key":"luz","label":"Verificar luz (CFE)"},{"key":"sat","label":"Cambio de domicilio: SAT"},{"key":"ine","label":"Cambio de domicilio: INE"},{"key":"bancos","label":"Cambio de domicilio: bancos"}]}]},"edificio":{"reglas":[{"icono":"🔊","titulo":"Ruido","detalle":"L-V 6am-11pm, S-D 7am-11pm. Fiestas: D-J hasta 12am, V-S hasta 3am"},{"icono":"📦","titulo":"Mudanzas","detalle":"L-D 9am-6pm, avisar 24h antes a la administración"},{"icono":"🐾","titulo":"Mascotas","detalle":"Correa en áreas comunes, hacer necesidades afuera del edificio"},{"icono":"🔨","titulo":"Remodelaciones","detalle":"L-V 9am-6pm, S 9am-2pm"},{"icono":"🅿️","titulo":"Estacionamiento","detalle":"Solo autos/motos/bicis, no bloquear acceso ni pasillos"}]},"galeria":[{"src":"fotos/fachada/FACHADA_C908140184_333541.jpeg","alt":"Fachada","label":"🏢 Fachada"},{"src":"fotos/entrada/ENTRADA PRINCIPAL_C908140184_333541.jpeg","alt":"Entrada","label":"🚪 Entrada"},{"src":"fotos/sala-comedor/SALA Y COMEDOR_C908140184_333541.jpeg","alt":"Sala","label":"🛋️ Sala / Comedor"},{"src":"fotos/cocina/COCINA_C908140184_333541.jpeg","alt":"Cocina","label":"🍳 Cocina"},{"src":"fotos/cuarto-1/CUARTO_01_CON CLOSET_C908140184_333541.jpeg","alt":"Cuarto 1","label":"🛏️ Recámara 1"},{"src":"fotos/cuarto-2/CUARTO_02_CON CLOSET_C908140184_333541.jpeg","alt":"Cuarto 2","label":"🛏️ Recámara 2"},{"src":"fotos/cuarto-3-master/CUARTO_03_CON CLOSET Y BANO COM PLETO_C908140184_333541 (4).jpeg","alt":"Master","label":"🛏️ Recámara Principal"},{"src":"fotos/cuarto-lavado/CUARTO DE LAVADO_C908140184_333541.jpeg","alt":"Lavado","label":"🧲 Cuarto de Lavado"},{"src":"fotos/pasillo/PASILLO A CUARTOS_C908140184_333541.jpeg","alt":"Pasillo","label":"🚶 Pasillo"},{"src":"fotos/estacionamiento/ESTACIONAMIENTO_C908140184_333541.jpeg","alt":"Estacionamiento","label":"🅿️ Estacionamiento"},{"src":"fotos/medidores/MEDIDOR DE AGUA_C908140184_333541.jpeg","alt":"Medidor agua","label":"💧 Medidor Agua"},{"src":"fotos/medidores/MEDIDOR DE LUZ_C908140184_333541 (2).jpeg","alt":"Medidor luz","label":"⚡ Medidor Luz"}],"notas_remodelacion":[{"icono":"🔧","titulo":"Piso laminado:","desc":"Revisar bordes despegados en sala/comedor"},{"icono":"🎨","titulo":"Pintura:","desc":"Paredes blancas en buen estado general, posible retoque"},{"icono":"🍳","titulo":"Cocina:","desc":"Falta campana/extractor y espacio para refri"},{"icono":"🛏️","titulo":"Closets:","desc":"Funcionales pero viejos — evaluar si se cambian o se mantienen"},{"icono":"🧹","titulo":"Limpieza:","desc":"Limpieza profunda necesaria antes de mudanza"}],"legal":{"documentos":[{"icono":"📜","nombre":"Escritura BBVA","tipo":"PDF","href":"docs/pdfs/escritura-bbva.pdf"},{"icono":"📋","nombre":"Reglamento del Condominio","tipo":"PDF","href":"docs/pdfs/reglamento-condominio.pdf"}],"puntos_cuestionables":[{"icono":"🌿","titulo":"Prohibición de cannabis dentro del depa","body":"SCJN 2021 declaró inconstitucional la prohibición absoluta. En áreas comunes es válido, pero dentro del depa es impugnable por derecho al libre desarrollo de la personalidad (art. 1° constitucional)."},{"icono":"🐕‍🦺","titulo":"Sin excepción para perros de asistencia","body":"La Ley de Propiedad en Condominio de CDMX obliga a permitir perros de asistencia sin restricciones. El reglamento omite esta excepción, lo que podría resultar en discriminación."},{"icono":"🔨","titulo":"Sanciones unilaterales por administración","body":"Las sanciones condominales deben ser aprobadas por la Asamblea de Condóminos (arts. 64, 69 de la Ley). La administración sola no tiene facultad para imponer sanciones económicas."},{"icono":"📞","titulo":"\"3 llamadas antes de llamar a policía\"","body":"Cualquier ciudadano puede llamar a autoridades en cualquier momento. El reglamento no puede condicionar ni restringir este derecho constitucional."}],"prosoc":{"titulo":"📞 Prosoc CDMX — Mediación gratuita","telefono":"Tel: 55 5128 5200 ext. 401 · Puebla 182, piso 4, Col. Roma Norte, Cuauhtémoc","web":"prosoc.cdmx.gob.mx"}},"eventos":[{"fecha":"Abr 12","texto":"Dashboard creado — tracker, checklist, legal, finanzas","tier":"t1","tierLabel":"Confirmado"},{"fecha":"Abr 11","texto":"Contexto completo del condominio documentado","tier":"t1","tierLabel":"Confirmado"},{"fecha":"Abr 11","texto":"Carta de instrucción notarial emitida por Notaría 211","tier":"t1","tierLabel":"Confirmado"},{"fecha":"Abr 11","texto":"Faltante actualizado a $900K (antes $600K)","tier":"t1","tierLabel":"Confirmado"},{"fecha":"Abr 11","texto":"Boleta de jornada notarial pendiente de firma","tier":"t2","tierLabel":"Pendiente"},{"fecha":"Abr 11","texto":"Mapa de estacionamientos — incierto si asignación es correcta","tier":"t3","tierLabel":"Sin confirmar"},{"fecha":"Abr 11","texto":"Predial 2026 pagado — $9,520 (6 bimestres)","tier":"t1","tierLabel":"Confirmado"},{"fecha":"~Ene","texto":"Promesa de compraventa firmada","tier":"t1","tierLabel":"Confirmado"},{"fecha":"~Ene","texto":"Avalúo realizado","tier":"t1","tierLabel":"Confirmado"},{"fecha":"~Ene","texto":"Anticipo de notaría pagado","tier":"t1","tierLabel":"Confirmado"}],"contactos":[{"icono":"🏛️","nombre":"Notaría 211","detalles":["Jornada notarial activada","Boleta pendiente de firma"]},{"icono":"🏦","nombre":"BBVA México","detalles":["Crédito $3,000,000","Middle office pendiente"]},{"icono":"⚖️","nombre":"Prosoc CDMX","telefono":"55 5128 5200 ext. 401","detalles":["Puebla 182, piso 4, Roma Norte","Mediación gratuita"]},{"icono":"🏢","nombre":"Administración","detalles":["Xola 1360","Contacto pendiente"]},{"icono":"💰","nombre":"Tesorería CDMX","telefono":"55 5588 3388","detalles":["finanzas.cdmx.gob.mx"]},{"icono":"📋","nombre":"Cuenta Predial","detalles":["026103160086","Uso-Rango-Clase: H-10-4"]}],"remodelacion_default":[{"concepto":"Pintura general","estimado":15000,"real":0,"status":"pendiente"},{"concepto":"Piso laminado reparación","estimado":8000,"real":0,"status":"pendiente"},{"concepto":"Limpieza profunda","estimado":3000,"real":0,"status":"pendiente"},{"concepto":"Cocina (campana/alacenas)","estimado":12000,"real":0,"status":"pendiente"},{"concepto":"Closets (revisión)","estimado":5000,"real":0,"status":"pendiente"}],"notificaciones":{"firmaDate":"2026-05-25","entregaDate":"2026-06-20"},"moodboard":{"rooms":[{"value":"sala","label":"🛋️ Sala"},{"value":"cocina","label":"🍳 Cocina"},{"value":"cuarto","label":"🛏️ Cuarto"},{"value":"oficina-art","label":"💻 Oficina Art"},{"value":"oficina-pame","label":"💻 Oficina Pame"},{"value":"bano","label":"🚿 Baño"}],"styleTags":["Moderno","Minimalista","Industrial","Escandinavo","Bohemio","Clásico"]}}; function renderDashboard(d){ window.__CASITA_DATA=d; // Header address const addr=$('#header-address'); if(addr) addr.textContent='📍 '+d.propiedad.direccion; // Updated badge const upd=$('#updated-badge'); if(upd) upd.textContent='Actualizado: '+d.meta.actualizado; // KPIs const kpis=$('#kpis-container'); if(kpis) kpis.innerHTML=d.kpis.map(k=>'
            '+k.valor+'
            '+k.label+'
            '+k.sub+'
            ').join(''); // Digest const dig=$('#digest-container'); if(dig) dig.innerHTML=d.digest.icono+' '+d.digest.texto; // Timeline summary const tls=$('#timeline-summary'); if(tls) tls.textContent=d.timelineSummary; // Timeline const tlc=$('#timeline-container'); if(tlc) tlc.innerHTML=d.timeline.map(t=>'
            '+t.fecha+'
            '+t.titulo+'
            '+t.desc+'
            ').join(''); // Property grid const pg=$('#prop-grid-container'); if(pg){ const p=d.propiedad; pg.innerHTML='
            '+p.precioDisplay+'
            Precio
            '+'
            '+p.superficie+'
            Superficie
            '+'
            '+p.recamaras+'
            Recámaras
            '+'
            '+p.estacionamientos+'
            Estacionamientos
            '+'
            '+p.predial+'
            Predial 2026
            '+'
            '+p.valorCatastral+'
            Valor catastral
            '; } // Flow steps const fc=$('#flow-container'); if(fc) fc.innerHTML=d.flujo_bbva.map(s=>{ const cls=s.status==='done'?' done':s.status==='current'?' current':''; const dot=s.status==='done'?'✓':String(s.paso); const label=s.nombre.replace(/\n/g,'
            '); return '
            '+dot+'
            '+label+'
            '; }).join(''); // Risks const rc=$('#risks-container'); if(rc) rc.innerHTML=d.riesgos.map(r=>'
            '+r.icono+'
            '+r.titulo+'
            '+r.desc+'
            ').join(''); // Finanzas docs const fdl=$('#finanzas-docs-links'); if(fdl) fdl.innerHTML=d.finanzas.documentos.map(doc=>''+doc.icono+' '+doc.nombre+' '+doc.tipo+'').join(''); // Finanzas precio total const fpt=$('#fin-precio-total'); if(fpt) fpt.textContent='$'+d.finanzas.precioTotal.toLocaleString(); // Finanzas structure + predial const frc=$('#fin-row-container'); if(frc){ const f=d.finanzas; frc.innerHTML='
            Estructura
            ' +'
            Crédito BBVA$'+f.credito.monto.toLocaleString()+'
            ' +'
            Ahorro disponible$'+f.ahorro.monto.toLocaleString()+'
            ' +'
            Faltante$'+f.faltante.monto.toLocaleString()+'
            ' +'
            ' +'
            Predial 2026
            ' +'
            $'+f.predial.monto.toLocaleString()+'
            Pagado · '+f.predial.bimestres+' bimestres
            ' +'
            $'+f.predial.catastral.toLocaleString()+'
            Valor catastral
            ' +'
            $'+f.predial.fiscal.toLocaleString()+'
            Valor fiscal
            ' +'
            '; } // Escenarios const esc=$('#escenarios-container'); if(esc) esc.innerHTML=d.finanzas.escenarios.map((e,i)=>{ const border=iAhorrando $'+(e.mensual/1000)+'K/mes'; }).join(''); // Checklist const ckc=$('#checklist-container'); if(ckc) ckc.innerHTML=d.checklist.grupos.map(g=>'
            '+g.titulo+'
            ').join(''); // Calendar const cmc=$('#cal-months-container'); if(cmc) cmc.innerHTML=d.calendario.map(m=>'
            '+(m.current?m.icono+' ':'')+m.mes+'
            '+m.eventos.map(e=>'
            '+e.texto+'
            ').join('')+'
            ').join(''); // Deadlines const dlc=$('#deadlines-container'); if(dlc) dlc.innerHTML=d.deadlines.map((dl,i)=>{ const border=i'+dl.icono+' '+dl.texto+''+dl.estado+''; }).join(''); // Building rules const brc=$('#building-rules-container'); if(brc) brc.innerHTML=d.edificio.reglas.map(r=>'
          • '+r.icono+''+r.titulo+''+r.detalle+'
          • ').join(''); // Gallery const gc=$('#gallery-container'); if(gc) gc.innerHTML=d.galeria.map(g=>'').join(''); // Project data const pd=$('#proyecto-datos'); if(pd){ const p=d.propiedad; pd.innerHTML=[ ['Proyecto',p.proyecto],['Dirección',p.direccionCompleta],['Escala',p.escala], ['Fecha',p.fecha],['Superficie',p.superficie+' (aprox.)'],['Propietario',p.propietario] ].map(r=>''+r[0]+':'+r[1]+'').join(''); } // Remodel notes const rnc=$('#remodel-notes-container'); if(rnc) rnc.innerHTML=d.notas_remodelacion.map(n=>'
          • '+n.icono+' '+n.titulo+' '+n.desc+'
          • ').join(''); // Legal docs const ldl=$('#legal-docs-links'); if(ldl) ldl.innerHTML=d.legal.documentos.map(doc=>''+doc.icono+' '+doc.nombre+' '+doc.tipo+'').join(''); // Legal items const lic=$('#legal-items-container'); if(lic){ let legalHtml='
            Puntos cuestionables del reglamento
            '; d.legal.puntos_cuestionables.forEach(p=>{ legalHtml+=''; }); lic.innerHTML=legalHtml; } // Prosoc const psc=$('#prosoc-container'); if(psc){ const pr=d.legal.prosoc; psc.innerHTML=''+pr.titulo+'

            '+pr.telefono+'

            '+pr.web+'

            '; } // Event log const elc=$('#event-log-container'); if(elc) elc.innerHTML=d.eventos.map(e=>'
            '+e.fecha+'
            '+e.texto+'
            '+e.tierLabel+'
            ').join(''); // Contacts const ctc=$('#contacts-container'); if(ctc) ctc.innerHTML=d.contactos.map(c=>{ let html='
            '+c.icono+' '+c.nombre+'
            '; if(c.telefono) html+='
            '+c.telefono+'
            '; (c.detalles||[]).forEach(det=>{ html+='
            '+det+'
            '; }); html+='
            '; return html; }).join(''); // Mood board rooms const mr=$('#mood-room'); if(mr&&d.moodboard) mr.innerHTML=d.moodboard.rooms.map(r=>'').join(''); // Mood board style tags const mt=$('#mood-tags'); if(mt&&d.moodboard) mt.innerHTML=d.moodboard.styleTags.map(t=>'').join(''); } // Load data: try fetch, fallback to inline (function loadData(){ fetch('data.json').then(r=>{if(!r.ok)throw new Error();return r.json();}).then(d=>{ window._dashData=d; renderDashboard(d); initAfterRender(); }).catch(()=>{ window._dashData=FALLBACK_DATA; renderDashboard(FALLBACK_DATA); initAfterRender(); }); })(); function initAfterRender(){ // Re-init checklist with localStorage const saved=JSON.parse(localStorage.getItem('casita_ck')||'{}'); $$('.checklist li').forEach(li=>{ const k=li.dataset.key; if(saved[k]){li.classList.add('checked');li.querySelector('.ck-box').textContent='✓';} else{li.querySelector('.ck-box').textContent='';li.classList.remove('checked');} li.addEventListener('click',()=>{ li.classList.toggle('checked'); const c=li.classList.contains('checked'); li.querySelector('.ck-box').textContent=c?'✓':''; saved[k]=c; localStorage.setItem('casita_ck',JSON.stringify(saved)); updateHealthScore(); }); }); // Pre-check known completed items on first visit if(!localStorage.getItem('casita_ck')){const done={'promesa':true,'carta':true,'avaluo':true,'anticipo':true};localStorage.setItem('casita_ck',JSON.stringify(done));Object.keys(done).forEach(k=>{const li=document.querySelector('[data-key="'+k+'"]');if(li){li.classList.add('checked');li.querySelector('.ck-box').textContent='✓';}});} // Re-init notes per step (uses outer stepNotes variable) for(const[s,v]of Object.entries(stepNotes)){ const ta=$('#note-'+s+' textarea'); if(ta){ta.value=v;if(v)$('#note-'+s).classList.add('open');} } // Init mood board after render (options are now in DOM) if(typeof initMoodBoardOnLoad==='function') initMoodBoardOnLoad(); updateHealthScore(); } function updateHealthScore(){ const d = window.__CASITA_DATA || FALLBACK_DATA; // 1. Checklist completion (40%) const saved = JSON.parse(localStorage.getItem('casita_ck') || '{}'); const totalItems = document.querySelectorAll('.checklist li').length || 1; const checkedItems = Object.values(saved).filter(Boolean).length; const ckScore = (checkedItems / totalItems) * 40; // 2. Finanzas health (25%) — 1 - (faltante / precio) const faltante = d.finanzas.faltante.monto; const precio = d.finanzas.precioTotal; const finScore = Math.max(0, (1 - faltante / precio)) * 25; // 3. Communication recency (20%) — based on most recent comm entry in localStorage let commScore = 0; const comms = JSON.parse(localStorage.getItem('casita-comunicacion') || '[]'); const oldComms = JSON.parse(localStorage.getItem('casita_comms') || '[]'); let lastDate = null; comms.forEach(c => { if(c.fecha) { const dt = new Date(c.fecha); if(!lastDate || dt > lastDate) lastDate = dt; }}); oldComms.forEach(c => { if(c.date) { const dt = new Date(c.date); if(!lastDate || dt > lastDate) lastDate = dt; }}); if(lastDate){ const daysSince = (Date.now() - lastDate.getTime()) / 86400000; if(daysSince <= 3) commScore = 20; else if(daysSince <= 7) commScore = 15; else if(daysSince <= 14) commScore = 10; else if(daysSince <= 30) commScore = 5; else commScore = 0; } // 4. Deadline proximity (15%) — days until firma date let dlScore = 0; const firmaStr = d.notificaciones && d.notificaciones.firmaDate; if(firmaStr){ const daysUntil = (new Date(firmaStr).getTime() - Date.now()) / 86400000; if(daysUntil > 30) dlScore = 15; else if(daysUntil > 14) dlScore = 12; else if(daysUntil > 7) dlScore = 8; else if(daysUntil > 0) dlScore = 4; else dlScore = 0; } const total = Math.round(ckScore + finScore + commScore + dlScore); const score = Math.max(0, Math.min(100, total)); // Update SVG const circumference = 2 * Math.PI * 52; const offset = circumference - (score / 100) * circumference; const arc = document.getElementById('hs-arc'); const val = document.getElementById('hs-val'); if(arc){ arc.style.strokeDashoffset = offset; arc.style.stroke = score > 70 ? 'var(--green)' : score >= 40 ? 'var(--amber)' : 'var(--red)'; } if(val) val.textContent = score; // "Next action" hint const hints = []; if(ckScore < 40){ const nextCkScore = Math.round(Math.min(40, ((checkedItems+1)/totalItems)*40) + finScore + commScore + dlScore); hints.push({gain: nextCkScore - score, text: 'Completa un paso del checklist \u2192 sube a ' + Math.min(100,nextCkScore)}); } if(commScore < 20){ const nextCommScore = Math.round(ckScore + finScore + 20 + dlScore); hints.push({gain: nextCommScore - score, text: 'Registra una comunicaci\u00f3n \u2192 sube a ' + Math.min(100,nextCommScore)}); } hints.sort((a,b) => b.gain - a.gain); const nextEl = document.getElementById('hs-next'); if(nextEl) nextEl.textContent = hints.length ? '\uD83D\uDCA1 ' + hints[0].text : ''; } // Auth handled by Firebase (see module script above) // Legacy hash auth removed — Firebase Auth manages login state // Tabs function trackTabUsage(tabName) { try { const raw = localStorage.getItem('casita_tab_usage'); const usage = raw ? JSON.parse(raw) : {}; if (!usage[tabName]) usage[tabName] = { count: 0, last: 0 }; usage[tabName].count++; usage[tabName].last = Date.now(); localStorage.setItem('casita_tab_usage', JSON.stringify(usage)); } catch(e) { /* localStorage unavailable or full */ } } function showTab(t){ const tabMap = { 'checklist':'progreso','timeline':'progreso','calendario':'progreso', 'edificio':'depa','fotos':'depa','planos':'depa', 'log':'legal','contactos':'legal', 'remodelacion':'tools', 'simulador':'finanzas' }; if(t==='moodboard'||t==='cotizador'||t==='ruta'||t==='inventario'||t==='comunicacion'){/* direct mapping, skip tabMap */} else { t = tabMap[t] || t; } trackTabUsage(t); $$('.tab').forEach(el=>el.classList.remove('active')); $$('.overflow-item').forEach(el=>el.classList.remove('active')); $$('.panel').forEach(el=>el.classList.remove('active')); // Activate the clicked tab in the bar or overflow menu let activated = false; if(event && event.target) { event.target.classList.add('active'); activated = true; } if(!activated) { $$('.tab').forEach(el=>{ if(el.dataset.tab === t) { el.classList.add('active'); activated = true; } }); } // Also highlight overflow item if tab is in overflow $$('.overflow-item').forEach(el=>{ if(el.dataset.tab === t) el.classList.add('active'); }); // If activated from overflow, also mark the overflow trigger const overflowTrigger = $('#tab-overflow-trigger'); if(overflowTrigger) { const overflowItems = $$('.overflow-item'); const anyOverflowActive = Array.from(overflowItems).some(el => el.classList.contains('active')); if(anyOverflowActive) overflowTrigger.classList.add('active'); else overflowTrigger.classList.remove('active'); } const panel = $(`#p-${t}`); if(panel) panel.classList.add('active'); // Re-render floor plan when Depa tab is shown (in case data wasn't ready on load) if(t === 'depa' && typeof renderFloorPlan === 'function') renderFloorPlan(); // Close overflow menu after selection const menu = $('#overflow-menu'); if(menu) menu.classList.remove('open'); } // Tab memory: reorder tabs by usage function initTabMemory() { const MIN_SWITCHES = 50; const MIN_AGE_MS = 14 * 24 * 60 * 60 * 1000; // 2 weeks const MIN_USE_THRESHOLD = 3; let usage; try { const raw = localStorage.getItem('casita_tab_usage'); usage = raw ? JSON.parse(raw) : null; } catch(e) { return; } if (!usage) return; // Check total switches and data age const entries = Object.values(usage); const totalSwitches = entries.reduce((s, e) => s + e.count, 0); const earliestLast = entries.reduce((m, e) => Math.min(m, e.last), Date.now()); const dataAge = Date.now() - earliestLast; if (totalSwitches < MIN_SWITCHES && dataAge < MIN_AGE_MS) return; const tabsBar = $('#tabs-bar'); if (!tabsBar) return; const allTabs = Array.from(tabsBar.querySelectorAll('.tab[data-tab]')); if (allTabs.length === 0) return; // Sort by count descending (tabs not in usage get count 0) const sorted = allTabs.slice().sort((a, b) => { const ca = (usage[a.dataset.tab] || {}).count || 0; const cb = (usage[b.dataset.tab] || {}).count || 0; return cb - ca; }); // Split into visible (count >= threshold) and overflow (count < threshold) const visible = []; const overflow = []; sorted.forEach(tab => { const count = (usage[tab.dataset.tab] || {}).count || 0; if (count >= MIN_USE_THRESHOLD) visible.push(tab); else overflow.push(tab); }); // Rebuild tab bar tabsBar.innerHTML = ''; visible.forEach(tab => tabsBar.appendChild(tab)); // Create overflow menu if needed if (overflow.length > 0) { const overflowContainer = document.createElement('div'); overflowContainer.className = 'tab tab-overflow'; overflowContainer.id = 'tab-overflow-trigger'; overflowContainer.textContent = '\u22EF M\u00E1s'; overflowContainer.onclick = function(e) { e.stopPropagation(); const menu = $('#overflow-menu'); if (menu) menu.classList.toggle('open'); }; const menu = document.createElement('div'); menu.className = 'overflow-menu'; menu.id = 'overflow-menu'; overflow.forEach(tab => { const item = document.createElement('button'); item.className = 'overflow-item'; item.dataset.tab = tab.dataset.tab; item.textContent = tab.textContent; item.onclick = function(e) { e.stopPropagation(); showTab(tab.dataset.tab); }; // If this tab was active, mark it if (tab.classList.contains('active')) item.classList.add('active'); menu.appendChild(item); }); // Reset button const resetBtn = document.createElement('button'); resetBtn.className = 'overflow-reset'; resetBtn.textContent = 'Restablecer orden'; resetBtn.onclick = function(e) { e.stopPropagation(); localStorage.removeItem('casita_tab_usage'); location.reload(); }; menu.appendChild(resetBtn); overflowContainer.appendChild(menu); tabsBar.appendChild(overflowContainer); // Close menu when clicking elsewhere document.addEventListener('click', function() { menu.classList.remove('open'); }); } // Show hint const hint = $('#tab-usage-hint'); if (hint) hint.classList.add('visible'); // If the currently active tab ended up in overflow, activate General instead const activeInBar = tabsBar.querySelector('.tab.active[data-tab]'); if (!activeInBar) { const firstTab = tabsBar.querySelector('.tab[data-tab]'); if (firstTab) { firstTab.classList.add('active'); const panel = $(`#p-${firstTab.dataset.tab}`); if (panel) { $$('.panel').forEach(p=>p.classList.remove('active')); panel.classList.add('active'); } } } } // Initialize tab memory on load document.addEventListener('DOMContentLoaded', initTabMemory); // Theme toggle function toggleTheme(){ const html=document.documentElement; const isLight=html.classList.toggle('light'); $('#themeToggle').textContent=isLight?'☀️':'🌙'; localStorage.setItem('casita_theme',isLight?'light':'dark'); } (function initTheme(){ const saved=localStorage.getItem('casita_theme'); const preferLight=saved ? saved==='light' : window.matchMedia('(prefers-color-scheme: light)').matches; if(preferLight){document.documentElement.classList.add('light');$('#themeToggle').textContent='☀️';} })(); // Notes per step const stepNotes=JSON.parse(localStorage.getItem('casita_notes')||'{}'); function toggleNote(e,step){ e.stopPropagation(); const area=$(`#note-${step}`); area.classList.toggle('open'); } function saveNote(step,val){ stepNotes[step]=val; localStorage.setItem('casita_notes',JSON.stringify(stepNotes)); } // Lightbox function openLB(el){ const img=el.querySelector('img'); const label=el.querySelector('.gallery-label'); $('#lb-img').src=img.src; $('#lb-cap').textContent=label?label.textContent:''; $('#lightbox').classList.add('open'); } // PWA if('serviceWorker' in navigator){navigator.serviceWorker.register('sw.js').catch(()=>{});} // ═══ INVENTARIO ═══ const invData=JSON.parse(localStorage.getItem('casita_inventario')||'{"lleva":[],"vende":[],"tira":[]}'); function renderInv(cat){ const ul=$(`#inv-list-${cat}`); ul.innerHTML=''; (invData[cat]||[]).forEach((item,i)=>{ const li=document.createElement('li'); li.style.cssText='display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--surface);border-radius:6px;margin-bottom:4px;font-size:0.85rem;'; li.innerHTML=`${item}`; ul.appendChild(li); }); $(`#inv-count-${cat}`).textContent=invData[cat].length; } function addInvItem(cat){ const inp=$(`#inv-input-${cat}`); const val=inp.value.trim(); if(!val)return; if(!invData[cat])invData[cat]=[]; invData[cat].push(val); inp.value=''; localStorage.setItem('casita_inventario',JSON.stringify(invData)); renderInv(cat); } function delInvItem(cat,idx){ invData[cat].splice(idx,1); localStorage.setItem('casita_inventario',JSON.stringify(invData)); renderInv(cat); } ['lleva','vende','tira'].forEach(renderInv); // ═══ COMUNICACIÓN ═══ const commData=JSON.parse(localStorage.getItem('casita_comms')||'[]'); (function initCommDate(){const d=new Date();$('#comm-date').value=d.toISOString().split('T')[0];})(); function renderComms(){ const log=$('#comm-log'); log.innerHTML=''; const whoIcons={'Notaría':'🏛️','BBVA':'🏦','Admin':'🏢','Prosoc':'⚖️','Otro':'📋'}; commData.sort((a,b)=>new Date(b.date)-new Date(a.date)); commData.forEach((c,i)=>{ const div=document.createElement('div'); div.style.cssText='display:flex;gap:12px;padding:12px 14px;border-radius:8px;margin-bottom:6px;background:var(--surface);align-items:flex-start;'; div.innerHTML=`
            ${c.date}
            ${whoIcons[c.who]||'📋'} ${c.who}
            ${c.note}
            `; log.appendChild(div); }); if(commData.length===0)log.innerHTML='
            No hay comunicaciones registradas
            '; } function addComm(){ const date=$('#comm-date').value; const who=$('#comm-who').value; const note=$('#comm-note').value.trim(); if(!note)return; commData.push({date,who,note}); localStorage.setItem('casita_comms',JSON.stringify(commData)); $('#comm-note').value=''; renderComms(); } function delComm(idx){ commData.splice(idx,1); localStorage.setItem('casita_comms',JSON.stringify(commData)); renderComms(); } renderComms(); // ═══ REMODELACIÓN ═══ const defaultRemodel=window.__CASITA_DATA?window.__CASITA_DATA.remodelacion_default:[ {concepto:'Pintura general',estimado:15000,real:0,status:'pendiente'}, {concepto:'Piso laminado reparación',estimado:8000,real:0,status:'pendiente'}, {concepto:'Limpieza profunda',estimado:3000,real:0,status:'pendiente'}, {concepto:'Cocina (campana/alacenas)',estimado:12000,real:0,status:'pendiente'}, {concepto:'Closets (revisión)',estimado:5000,real:0,status:'pendiente'} ]; let remodelData=JSON.parse(localStorage.getItem('casita_remodel')||'null')||defaultRemodel; function saveRemodel(){localStorage.setItem('casita_remodel',JSON.stringify(remodelData));} function renderRemodel(){ const tbody=$('#remodel-body'); tbody.innerHTML=''; let totalEst=0,totalReal=0; const statusColors={'pendiente':'var(--orange)','en proceso':'var(--amber)','listo':'var(--green)'}; const statusBgs={'pendiente':'var(--orange-dim)','en proceso':'var(--gold-glow)','listo':'var(--green-dim)'}; remodelData.forEach((item,i)=>{ totalEst+=item.estimado; totalReal+=item.real; const tr=document.createElement('tr'); tr.style.cssText='background:var(--surface);'; tr.innerHTML=`${item.concepto}$${item.estimado.toLocaleString()}$${item.real.toLocaleString()}`; tbody.appendChild(tr); }); const fmt=n=>'$'+n.toLocaleString(); $('#remodel-est-total').textContent=fmt(totalEst); $('#remodel-real-total').textContent=fmt(totalReal); $('#remodel-est-big').textContent=fmt(totalEst); $('#remodel-real-big').textContent=fmt(totalReal); const diff=totalEst-totalReal; const diffEl=$('#remodel-diff-big'); diffEl.textContent=(diff>=0?'+':'')+fmt(Math.abs(diff)); diffEl.style.color=diff>=0?'var(--green)':'var(--red)'; } function updateRemodel(el){ const idx=parseInt(el.dataset.idx); const field=el.dataset.field; const raw=el.textContent.replace(/[^0-9]/g,''); const val=parseInt(raw)||0; remodelData[idx][field]=val; saveRemodel(); renderRemodel(); } function updateRemodelStatus(idx,val){ remodelData[idx].status=val; saveRemodel(); renderRemodel(); } renderRemodel(); // ═══ GLOBAL BACKUP ═══ function exportGlobalBackup() { const keys = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('casita_')) { keys[key] = localStorage.getItem(key); } } const backup = { version: 1, created: new Date().toISOString(), source: 'dashboard', keys: keys }; const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0'); const filename = `casita-backup-${dateStr}.json`; const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); localStorage.setItem('casita_last_backup', new Date().toISOString()); alert(`✅ Backup exportado como ${filename}`); } function importGlobalBackup() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const parsed = JSON.parse(e.target.result); // Support both envelope format (v1) and legacy flat format const data = (parsed.version && parsed.keys) ? parsed.keys : parsed; const casitaKeys = Object.keys(data).filter(k => k.startsWith('casita_')); if (casitaKeys.length === 0) { alert('❌ El archivo no parece ser un backup válido de Casita (no hay claves casita_*)'); return; } const keyCount = casitaKeys.length; const message = `¿Importar backup con ${keyCount} configuraciones?\n\n⚠️ Esto sobrescribirá tus datos actuales.\n\nClaves encontradas: ${casitaKeys.slice(0, 5).join(', ')}${casitaKeys.length > 5 ? '...' : ''}`; if (!confirm(message)) return; let importedCount = 0; casitaKeys.forEach(key => { localStorage.setItem(key, data[key]); importedCount++; }); alert(`✅ Backup importado: ${importedCount} configuraciones.\n\nRecargando página...`); window.location.reload(); } catch (error) { console.error('Import error:', error); alert('❌ Error al leer el archivo: ' + error.message); } }; reader.readAsText(file); }; input.click(); } // Check backup reminder (function checkBackupReminder() { const last = localStorage.getItem('casita_last_backup'); const el = document.getElementById('backup-reminder'); if (!el) return; if (!last) { el.style.display = 'block'; return; } const daysSince = (Date.now() - new Date(last).getTime()) / (1000 * 60 * 60 * 24); if (daysSince >= 7) { el.style.display = 'block'; } })(); // ═══ GLOBAL SEARCH ═══ (function initSearch(){ const searchInput=$('#searchInput'); const searchResults=$('#searchResults'); const searchBar=$('#searchBar'); let debounceTimer=null; // Tab name map for display const tabLabels={ general:'General', finanzas:'Finanzas', checklist:'Checklist', timeline:'Timeline', legal:'Legal', log:'Event Log', contactos:'Contactos', edificio:'Edificio', fotos:'Fotos', planos:'Planos', simulador:'Simulador', calendario:'Calendario', inventario:'Inventario', comunicacion:'Comunicación', remodelacion:'Remodelación' }; function getSearchableItems(){ const items=[]; // Helper to add items function add(text, tabId, el){ if(text && text.trim()) items.push({text:text.trim(), tabId, el}); } // General panel const pGen=$('#p-general'); if(pGen){ pGen.querySelectorAll('.prop-stat').forEach(el=>{ const val=el.querySelector('.val'); const lbl=el.querySelector('.lbl'); if(val&&lbl) add(val.textContent+' — '+lbl.textContent,'general',el); }); pGen.querySelectorAll('.risk').forEach(el=>{ const t=el.querySelector('.title'); const d=el.querySelector('.desc'); add((t?t.textContent:'')+' '+(d?d.textContent:''),'general',el); }); pGen.querySelectorAll('.flow-step').forEach(el=>{ const lbl=el.querySelector('.flow-label'); if(lbl) add(lbl.textContent.replace(/
            /g,' '),'general',el); }); } // Finanzas const pFin=$('#p-finanzas'); if(pFin){ pFin.querySelectorAll('.fin-bar-wrap').forEach(el=>{ const lbl=el.querySelector('.fin-bar-label'); const val=el.querySelector('.fin-bar-value'); if(lbl&&val) add(lbl.textContent+' — '+val.textContent,'finanzas',el); }); pFin.querySelectorAll('.prop-stat').forEach(el=>{ const val=el.querySelector('.val'); const lbl=el.querySelector('.lbl'); if(val&&lbl) add(val.textContent+' — '+lbl.textContent,'finanzas',el); }); pFin.querySelectorAll('.fin-big').forEach(el=>{ const a=el.querySelector('.amount'); const l=el.querySelector('.label'); if(a&&l) add(a.textContent+' — '+l.textContent,'finanzas',el); }); } // Checklist $$('#p-checklist .checklist li').forEach(el=>{ const lbl=el.querySelector('.ck-label'); if(lbl) add(lbl.textContent,'checklist',el); }); $$('#p-checklist .check-group-title').forEach(el=>{ add(el.textContent,'checklist',el); }); // Timeline $$('#p-timeline .tl-item').forEach(el=>{ const date=el.querySelector('.tl-date'); const title=el.querySelector('.tl-title'); const desc=el.querySelector('.tl-desc'); add((date?date.textContent+' — ':'')+(title?title.textContent:'')+(desc?' — '+desc.textContent:''),'timeline',el); }); // Legal $$('#p-legal .legal-item').forEach(el=>{ const head=el.querySelector('.legal-head'); const body=el.querySelector('.legal-body p'); add((head?head.textContent.replace('▼','').trim():'')+' '+(body?body.textContent:''),'legal',el); }); const prosoc=$('#p-legal .prosoc-card'); if(prosoc) add(prosoc.textContent,'legal',prosoc); // Event Log $$('#p-log .event').forEach(el=>{ const date=el.querySelector('.ev-date'); const text=el.querySelector('.ev-text'); add((date?date.textContent+' — ':'')+(text?text.textContent:''),'log',el); }); // Contactos $$('#p-contactos .contact').forEach(el=>{ add(el.textContent.trim().replace(/\s+/g,' '),'contactos',el); }); // Edificio $$('#p-edificio .building-rules li').forEach(el=>{ add(el.textContent.trim().replace(/\s+/g,' '),'edificio',el); }); // Calendario $$('#p-calendario .cal-event').forEach(el=>{ const month=el.closest('.cal-month'); const monthName=month?month.querySelector('.month-name'):null; add((monthName?monthName.textContent.replace('🟢','').trim()+' — ':'')+el.textContent.trim(),'calendario',el); }); // Calendar deadlines const pCal=$('#p-calendario'); if(pCal){ pCal.querySelectorAll('[style*="display:flex"][style*="justify-content:space-between"]').forEach(el=>{ if(el.closest('.cal-months')) return; add(el.textContent.trim().replace(/\s+/g,' '),'calendario',el); }); } // Remodelación items from localStorage const rItems=JSON.parse(localStorage.getItem('casita_remodel')||'null')||[]; rItems.forEach((item,i)=>{ const row=$('#remodel-body'); const trs=row?row.querySelectorAll('tr'):[]; add(item.concepto+' — Est: $'+item.estimado.toLocaleString()+' Real: $'+item.real.toLocaleString()+' ('+item.status+')','remodelacion',trs[i]||row); }); // Planos notes $$('#p-planos li').forEach(el=>{ add(el.textContent.trim(),'planos',el); }); // Digest const digest=$('.digest'); if(digest) add(digest.textContent.trim(),'general',digest); return items; } function highlightMatch(text, query){ const escaped=query.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); const re=new RegExp('('+escaped+')','gi'); return text.replace(re,'$1'); } function doSearch(){ const query=searchInput.value.trim(); if(query.length<2){ searchResults.classList.remove('visible'); searchResults.innerHTML=''; return; } const items=getSearchableItems(); const lowerQ=query.toLowerCase(); const matches=items.filter(item=>item.text.toLowerCase().includes(lowerQ)); if(matches.length===0){ searchResults.innerHTML='
            No se encontraron resultados para "'+query+'"
            '; searchResults.classList.add('visible'); return; } // Limit to 20 results const shown=matches.slice(0,20); searchResults.innerHTML=shown.map((m,i)=>{ // Truncate text around match for display const idx=m.text.toLowerCase().indexOf(lowerQ); let snippet=m.text; if(snippet.length>120){ const start=Math.max(0,idx-40); const end=Math.min(snippet.length,idx+query.length+60); snippet=(start>0?'…':'')+snippet.slice(start,end)+(end'+ '
            '+highlightMatch(snippet,query)+'
            '+ ''+(tabLabels[m.tabId]||m.tabId)+''; }).join(''); // Store matches for click handling searchResults._matches=shown; searchResults.classList.add('visible'); } // Click on result searchResults.addEventListener('click',function(e){ const item=e.target.closest('.search-result-item'); if(!item) return; const tabId=item.dataset.tab; const idx=parseInt(item.dataset.idx); const match=searchResults._matches&&searchResults._matches[idx]; // Switch tab $$('.tab').forEach(t=>t.classList.remove('active')); $$('.panel').forEach(p=>p.classList.remove('active')); const tabBtn=Array.from($$('.tab')).find(t=>t.getAttribute('onclick')&&t.getAttribute('onclick').includes(tabId)); if(tabBtn) tabBtn.classList.add('active'); const panel=$('#p-'+tabId); if(panel) panel.classList.add('active'); // Scroll to element if(match&&match.el){ setTimeout(()=>{ match.el.scrollIntoView({behavior:'smooth',block:'center'}); match.el.style.outline='2px solid var(--amber)'; match.el.style.outlineOffset='4px'; match.el.style.transition='outline-color 0.3s'; setTimeout(()=>{ match.el.style.outline='2px solid transparent'; setTimeout(()=>{match.el.style.outline='';match.el.style.outlineOffset='';},300); },1500); },100); } // Close search searchResults.classList.remove('visible'); }); // Debounced input searchInput.addEventListener('input',function(){ clearTimeout(debounceTimer); debounceTimer=setTimeout(doSearch,200); }); // ESC closes search searchInput.addEventListener('keydown',function(e){ if(e.key==='Escape'){ searchInput.value=''; searchResults.classList.remove('visible'); searchBar.classList.remove('open'); } }); // Close results when clicking outside document.addEventListener('click',function(e){ if(!searchBar.contains(e.target)&&!e.target.closest('.search-toggle')){ searchResults.classList.remove('visible'); } }); })(); function toggleSearch(){ const bar=$('#searchBar'); bar.classList.toggle('open'); if(bar.classList.contains('open')){ setTimeout(()=>$('#searchInput').focus(),50); } else { $('#searchInput').value=''; $('#searchResults').classList.remove('visible'); } } // ═══ MOOD BOARD ═══ const MOOD_KEY = 'casita_moodboard'; const defaultMoodRooms = (window.__CASITA_DATA&&window.__CASITA_DATA.moodboard?window.__CASITA_DATA.moodboard.rooms.map(r=>r.value):['sala','cocina','cuarto','oficina-art','oficina-pame','bano']); function getMoodData() { let data = JSON.parse(localStorage.getItem(MOOD_KEY) || 'null'); if (!data) { data = {}; defaultMoodRooms.forEach(r => { data[r] = { images: [], colors: ['#e0e0e0','#a78bfa','#4ade80','#fbbf24','#f87171'], notes: '', tags: [] }; }); localStorage.setItem(MOOD_KEY, JSON.stringify(data)); } return data; } function getCurrentMoodRoom() { const sel = $('#mood-room'); return sel ? sel.value : 'sala'; } function loadMoodBoard(room) { const data = getMoodData(); const rd = data[room] || { images: [], colors: ['#e0e0e0','#a78bfa','#4ade80','#fbbf24','#f87171'], notes: '', tags: [] }; // Colors const colorInputs = $$('#mood-colors input[type="color"]'); colorInputs.forEach((inp, i) => { inp.value = rd.colors[i] || '#000000'; }); // Notes const notesEl = $('#mood-notes'); if (notesEl) notesEl.value = rd.notes || ''; // Tags $$('#mood-tags .style-tag').forEach(btn => { btn.classList.toggle('active', (rd.tags || []).includes(btn.dataset.tag)); }); // Images renderMoodImages(rd.images); } function saveMoodBoard() { const room = getCurrentMoodRoom(); const data = getMoodData(); // Colors const colors = []; $$('#mood-colors input[type="color"]').forEach(inp => colors.push(inp.value)); // Notes const notes = ($('#mood-notes') || {}).value || ''; // Tags const tags = []; $$('#mood-tags .style-tag.active').forEach(btn => tags.push(btn.dataset.tag)); // Keep existing images const existing = data[room] || { images: [] }; data[room] = { images: existing.images || [], colors, notes, tags }; localStorage.setItem(MOOD_KEY, JSON.stringify(data)); } function renderMoodImages(images) { const grid = $('#mood-grid'); if (!grid) return; grid.innerHTML = ''; (images || []).forEach((img, i) => { const wrap = document.createElement('div'); wrap.className = 'mood-img-wrap'; wrap.innerHTML = `Inspiración`; grid.appendChild(wrap); }); } function openMoodLB(src) { const lb = $('#mood-lightbox'); const img = $('#mood-lb-img'); if (lb && img) { img.src = src; lb.classList.add('open'); } } function deleteMoodImage(idx) { const room = getCurrentMoodRoom(); const data = getMoodData(); if (data[room] && data[room].images) { data[room].images.splice(idx, 1); localStorage.setItem(MOOD_KEY, JSON.stringify(data)); renderMoodImages(data[room].images); } } function toggleStyleTag(btn) { btn.classList.toggle('active'); saveMoodBoard(); } function addImageFromUrl(url) { if (!url || !url.trim()) return; url = url.trim(); const room = getCurrentMoodRoom(); const data = getMoodData(); if (!data[room]) data[room] = { images: [], colors: [], notes: '', tags: [] }; data[room].images.push(url); localStorage.setItem(MOOD_KEY, JSON.stringify(data)); renderMoodImages(data[room].images); const inp = $('#mood-url-input'); if (inp) inp.value = ''; } function addImageFromFile(file) { if (!file || !file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = function(e) { const room = getCurrentMoodRoom(); const data = getMoodData(); if (!data[room]) data[room] = { images: [], colors: [], notes: '', tags: [] }; data[room].images.push(e.target.result); localStorage.setItem(MOOD_KEY, JSON.stringify(data)); renderMoodImages(data[room].images); }; reader.readAsDataURL(file); } function handleMoodFiles(files) { Array.from(files).forEach(f => addImageFromFile(f)); } // Drag and drop (function initMoodDragDrop() { const upload = document.getElementById('mood-upload'); if (!upload) return; ['dragenter','dragover'].forEach(ev => { upload.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); upload.classList.add('dragover'); }); }); ['dragleave','drop'].forEach(ev => { upload.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); upload.classList.remove('dragover'); }); }); upload.addEventListener('drop', e => { const files = e.dataTransfer.files; if (files.length) handleMoodFiles(files); }); })(); // Paste URL listener (function initMoodPaste() { const urlInput = document.getElementById('mood-url-input'); if (!urlInput) return; urlInput.addEventListener('paste', e => { setTimeout(() => { const val = urlInput.value.trim(); if (val && (val.startsWith('http://') || val.startsWith('https://'))) { addImageFromUrl(val); } }, 100); }); urlInput.addEventListener('keydown', e => { if (e.key === 'Enter') { addImageFromUrl(urlInput.value); } }); })(); // Init mood board (called from initAfterRender) function initMoodBoardOnLoad() { getMoodData(); // ensure data exists loadMoodBoard('sala'); } // NOTIFICATIONS function checkDeadlines(){ const notifs=[]; const now=new Date(); const nd=window.__CASITA_DATA&&window.__CASITA_DATA.notificaciones||{}; const firmaDate=new Date(nd.firmaDate||'2026-05-25'); const entregaDate=new Date(nd.entregaDate||'2026-06-20'); const diffFirma=Math.ceil((firmaDate-now)/(1000*60*60*24)); const diffEntrega=Math.ceil((entregaDate-now)/(1000*60*60*24)); if(diffFirma<=30&&diffFirma>0) notifs.push({type:'warn',text:`⏰ Firma estimada en ${diffFirma} días (~${Math.ceil(diffFirma/7)} semanas)`}); if(diffFirma<=0) notifs.push({type:'info',text:'✅ Fecha de firma alcanzada — ¿ya se firmó?'}); if(diffEntrega<=45&&diffEntrega>0) notifs.push({type:'info',text:`🏠 Entrega estimada en ${diffEntrega} días`}); // Check faltante notifs.push({type:'warn',text:'💰 Faltante: $900,000 — definir fuente de fondos'}); // Check boleta notifs.push({type:'warn',text:'⚖️ Boleta jornada notarial — pendiente de firma'}); const bar=$('#notifBar'); if(!bar||notifs.length===0)return; const dismissed=JSON.parse(localStorage.getItem('casita_notif_dismissed')||'[]'); const active=notifs.filter(n=>!dismissed.includes(n.text)); if(active.length===0)return; bar.innerHTML=active.map(n=>`
            ${n.text}
            `).join(''); bar.classList.add('show'); } function dismissNotif(el,encoded){ const text=decodeURIComponent(atob(encoded)); const d=JSON.parse(localStorage.getItem('casita_notif_dismissed')||'[]'); d.push(text);localStorage.setItem('casita_notif_dismissed',JSON.stringify(d)); el.closest('.notif-item').remove();checkNotifBar(); } function checkNotifBar(){if(!$('#notifBar').children.length)$('#notifBar').classList.remove('show');} setTimeout(checkDeadlines,1000); // ═══ ROOM AREAS (M² DISTRIBUTION) ═══ function renderRoomAreas(){ const grid=$('#room-areas-grid'); if(!grid)return; const raw=localStorage.getItem('casita_distribution'); if(!raw){ grid.innerHTML='

            Abre el editor de distribución para ver las áreas.

            '; return; } try{ const data=JSON.parse(raw); const rooms=data.rooms||data; if(!Array.isArray(rooms)||rooms.length===0){ grid.innerHTML='

            Abre el editor de distribución para ver las áreas.

            '; return; } // Shoelace formula for polygon area function shoelace(points){ let area=0; const n=points.length; for(let i=0;i{ let areaM2=0; const scale=data.scale||50; // px per meter, default 50 if(r.points&&r.points.length>=3){ // Polygon room const areaPx=shoelace(r.points); areaM2=areaPx/(scale*scale); } else if(r.width&&r.height){ // Rectangular room areaM2=(r.width/scale)*(r.height/scale); } if(areaM2<=0)return; const name=r.name||r.label||'Sin nombre'; const emoji=r.emoji||''; html+=`
            `; html+=`
            ${emoji} ${name}
            `; html+=`
            ${areaM2.toFixed(1)} m²
            `; html+=`
            `; }); if(!html){ grid.innerHTML='

            No se encontraron espacios con dimensiones.

            '; return; } grid.innerHTML=html; }catch(e){ grid.innerHTML='

            Error al leer datos de distribución.

            '; } } renderRoomAreas(); // Re-render when localStorage changes (e.g. from distribucion editor in another tab) window.addEventListener('storage',function(e){ if(e.key==='casita_distribution')renderRoomAreas(); }); // PRO AREA TABLE function renderProAreas(){ const raw=localStorage.getItem('casita_distribution'); const body=$('#pro-areas-body');const foot=$('#pro-areas-foot'); const empty=$('#pro-areas-empty');const tbl=$('#pro-areas-table'); if(!body)return; if(!raw){if(empty)empty.style.display='block';if(tbl)tbl.style.display='none';return;} const rooms=JSON.parse(raw); const PX=60; const types={'Sala':'Social','Comedor':'Social','Cocina':'Servicio','Cto':'Servicio', 'Oficina':'Privado','Cuarto':'Privado','Bano':'Servicio','Closet':'Privado', 'Pasillo':'Circulación','Baño':'Servicio'}; let html='';let total=0;let byType={}; rooms.forEach((r,i)=>{ let area=0; if(r.points){const pts=r.points;let a=0;for(let j=0;jr.name.includes(k)); const typeName=type?types[type]:'Otro'; byType[typeName]=(byType[typeName]||0)+area; total+=area; const bg=i%2===0?'transparent':'rgba(255,255,255,0.02)'; html+=`${i+1}${r.name}${area.toFixed(1)}${typeName}`; }); let footHtml=''; Object.entries(byType).sort((a,b)=>b[1]-a[1]).forEach(([t,a])=>{ footHtml+=`${t}${a.toFixed(1)}`; }); footHtml+=`TOTAL${total.toFixed(1)} m²`; body.innerHTML=html;foot.innerHTML=footHtml; if(tbl)tbl.style.display='table';if(empty)empty.style.display='none'; } // PRO OPENINGS TABLE function renderProOpenings(){ const raw=localStorage.getItem('casita_layers_openings'); const body=$('#pro-openings-body');const foot=$('#pro-openings-foot'); const empty=$('#pro-openings-empty');const tbl=$('#pro-openings-table'); if(!body)return; if(!raw||raw==='[]'){if(empty)empty.style.display='block';if(tbl)tbl.style.display='none';return;} const els=JSON.parse(raw); const typeMap={'door':'Puerta','double-door':'Puerta doble','window-s':'Ventana 0.6m','window':'Ventana 1.2m','window-m':'Ventana 1.8m','window-l':'Ventanal 2.4m','sliding':'Corrediza'}; let html='';let doors=0,windows=0; els.forEach((el,i)=>{ const tipo=typeMap[el.toolId]||el.toolId; const isDoor=el.toolId?.includes('door'); const isWindow=el.toolId?.includes('window')||el.toolId==='sliding'; if(isDoor)doors++;if(isWindow)windows++; const dx=(el.x2||0)-(el.x1||0);const dy=(el.y2||0)-(el.y1||0); const len=Math.sqrt(dx*dx+dy*dy)/60; const bg=i%2===0?'transparent':'rgba(255,255,255,0.02)'; html+=`${i+1}${tipo}${len.toFixed(1)}Aluminio`; }); const footHtml=`Total: ${doors} puertas, ${windows} ventanas${els.length}`; body.innerHTML=html;foot.innerHTML=footHtml; if(tbl)tbl.style.display='table';if(empty)empty.style.display='none'; } setTimeout(()=>{renderProAreas();renderProOpenings();},500); window.addEventListener('storage',()=>{renderProAreas();renderProOpenings();}); // BUDGET PER M² function renderBudget(){ const raw=localStorage.getItem('casita_distribution'); const body=$('#budget-body');const foot=$('#budget-foot'); const empty=$('#budget-empty');const tbl=$('#budget-table'); if(!body)return; if(!raw){if(empty)empty.style.display='block';if(tbl)tbl.style.display='none';return;} const rooms=JSON.parse(raw);const PX=60; // Cost per m² by room type (MXN, medium-high quality 2026) const costs={'Sala':8000,'Comedor':8000,'Cocina':15000,'Cto':6000, 'Oficina':10000,'Cuarto':8000,'Bano':18000,'Closet':5000, 'Pasillo':6000,'Baño':18000,'default':8000}; let html='';let grandTotal=0; rooms.forEach((r,i)=>{ let area=0; if(r.points){const pts=r.points;let a=0;for(let j=0;jr.name.includes(k))||'default'; const costPerM2=costs[costKey]; const total=area*costPerM2; grandTotal+=total; const bg=i%2===0?'transparent':'rgba(255,255,255,0.02)'; html+=`${r.name}${area.toFixed(1)}$${costPerM2.toLocaleString()}$${Math.round(total).toLocaleString()}`; }); const footHtml=`TOTAL ESTIMADO$${Math.round(grandTotal).toLocaleString()} *Estimado. Costos reales varían según materiales y mano de obra.`; body.innerHTML=html;foot.innerHTML=footHtml; if(tbl)tbl.style.display='table';if(empty)empty.style.display='none'; } setTimeout(renderBudget,600); window.addEventListener('storage',renderBudget); // ═══ RUTA CRÍTICA ═══ const RUTA_LS_KEY = 'casita-ruta-critica'; function getRutaStatuses() { return JSON.parse(localStorage.getItem(RUTA_LS_KEY) || '{}'); } function saveRutaStatuses(statuses) { localStorage.setItem(RUTA_LS_KEY, JSON.stringify(statuses)); } function renderRutaCritica() { const data = window.__CASITA_DATA; if (!data || !data.rutaCritica) return; const pasos = data.rutaCritica.pasos; const saved = getRutaStatuses(); const timeline = $('#ruta-timeline'); const banner = $('#ruta-next-step'); const progressFill = $('#ruta-progress-fill'); const progressText = $('#ruta-progress-text'); if (!timeline) return; // Merge saved statuses with data defaults const statuses = {}; pasos.forEach(p => { statuses[p.id] = saved[p.id] || p.status; }); // Helper: check if all dependencies are "listo" function depsReady(paso) { return paso.dependencias.every(depId => statuses[depId] === 'listo'); } // Find next undone step const nextStep = pasos.find(p => statuses[p.id] !== 'listo'); if (nextStep) { banner.textContent = nextStep.icono + ' ' + nextStep.nombre; } else { banner.textContent = '¡Todo listo! 🎉'; banner.style.color = 'var(--green)'; } // Progress const done = pasos.filter(p => statuses[p.id] === 'listo').length; const pct = Math.round((done / pasos.length) * 100); if (progressFill) progressFill.style.width = pct + '%'; if (progressText) progressText.textContent = done + ' de ' + pasos.length + ' completados (' + pct + '%)'; // Render steps timeline.innerHTML = ''; pasos.forEach(paso => { const status = statuses[paso.id]; const ready = depsReady(paso); const cssClass = status === 'listo' ? 'listo' : status === 'en progreso' ? 'en-progreso' : ''; const step = document.createElement('div'); step.className = 'ruta-step ' + cssClass; // Dot const dot = status === 'listo' ? '✓' : ''; // Status badge let badgeClass, badgeText; if (status === 'listo') { badgeClass = 'st-listo'; badgeText = 'Listo'; } else if (!ready) { badgeClass = 'st-bloqueado'; badgeText = '🔒 Bloqueado'; } else if (status === 'en progreso') { badgeClass = 'st-en-progreso'; badgeText = 'En progreso'; } else { badgeClass = 'st-pendiente'; badgeText = 'Pendiente'; } // Dependency names for blocked tooltip let tooltipHtml = ''; if (!ready && status !== 'listo') { const blockers = paso.dependencias .filter(depId => statuses[depId] !== 'listo') .map(depId => { const dep = pasos.find(p => p.id === depId); return dep ? dep.icono + ' ' + dep.nombre : depId; }); tooltipHtml = '
            🚫 Requiere: ' + blockers.join(', ') + '
            '; } // Dependency line text let depsHtml = ''; if (paso.dependencias.length > 0) { const depNames = paso.dependencias.map(depId => { const dep = pasos.find(p => p.id === depId); return dep ? dep.icono : depId; }); const allDepsOk = ready; depsHtml = '' + (allDepsOk ? '✅' : '🔒') + ' Depende de: ' + depNames.join(' ') + ''; } step.innerHTML = '
            ' + dot + '
            ' + '
            ' + '' + paso.icono + '' + '' + paso.nombre + '' + '' + '' + badgeText + '' + tooltipHtml + '' + '
            ' + '
            ' + '📅 ' + paso.fecha + '' + depsHtml + '
            '; timeline.appendChild(step); }); // Click handlers for status badges timeline.querySelectorAll('.ruta-status-badge').forEach(badge => { badge.addEventListener('click', function(e) { e.stopPropagation(); const pasoId = this.dataset.pasoId; const paso = pasos.find(p => p.id === pasoId); if (!paso) return; const currentStatus = statuses[pasoId]; const ready = depsReady(paso); // Cycle: pendiente -> en progreso -> listo -> pendiente // But only if dependencies are met for "listo" let newStatus; if (currentStatus === 'pendiente') { newStatus = ready ? 'en progreso' : 'pendiente'; if (!ready) return; // Can't advance if blocked } else if (currentStatus === 'en progreso') { newStatus = ready ? 'listo' : 'en progreso'; if (!ready) return; } else if (currentStatus === 'listo') { // Check if any dependent step is already started/done const hasDependents = pasos.some(p => p.dependencias.includes(pasoId) && statuses[p.id] !== 'pendiente' ); if (hasDependents) { // Can't undo — downstream steps depend on this return; } newStatus = 'pendiente'; } statuses[pasoId] = newStatus; saved[pasoId] = newStatus; saveRutaStatuses(saved); renderRutaCritica(); }); }); } // Render after data loads const _origInitAfterRender = initAfterRender; initAfterRender = function() { _origInitAfterRender(); renderRutaCritica(); }; // Also try immediately in case data is already loaded setTimeout(renderRutaCritica, 300); window.addEventListener('storage', function(e) { if (e.key === RUTA_LS_KEY) renderRutaCritica(); }); /* ── Deadline Banners (Spec #08) ── */ (function initDeadlineBanners(){ const STORAGE_KEY='casita-dismissed-deadlines'; function getDismissed(){try{return JSON.parse(sessionStorage.getItem(STORAGE_KEY)||'[]');}catch(e){return[];}} function dismiss(id){const d=getDismissed();d.push(id);sessionStorage.setItem(STORAGE_KEY,JSON.stringify(d));renderDeadlineBanners();} function calcLevel(fecha){ if(!fecha) return {cls:'db-asap db-pulse',text:'ASAP',priority:0}; const today=new Date();today.setHours(0,0,0,0); const target=new Date(fecha+'T00:00:00');target.setHours(0,0,0,0); const diff=Math.round((target-today)/(1000*60*60*24)); if(diff<0) return {cls:'db-red db-pulse',text:`VENCIDO (hace ${Math.abs(diff)} día${Math.abs(diff)===1?'':'s'})`,priority:0}; if(diff===0) return {cls:'db-red',text:'HOY',priority:1}; if(diff<=7) return {cls:'db-orange',text:`${diff} día${diff===1?'':'s'}`,priority:2}; if(diff<=14) return {cls:'db-yellow',text:`${diff} días`,priority:3}; return null;/* >14 days, no banner */ } window.renderDeadlineBanners=function(){ const container=$('#deadlineBanners'); if(!container||!window._dashData) return; const notif=window._dashData.notificaciones; if(!notif||!notif.deadlines) return; const dismissed=getDismissed(); const items=[]; notif.deadlines.forEach(function(dl){ if(dismissed.includes(dl.id)) return; const level=calcLevel(dl.fecha); if(!level) return; items.push({id:dl.id,icono:dl.icono,nombre:dl.nombre,cls:level.cls,text:level.text,priority:level.priority}); }); items.sort(function(a,b){return a.priority-b.priority;}); if(!items.length){container.innerHTML='';return;} container.innerHTML=items.map(function(it){ const label=it.text.startsWith('VENCIDO')||it.text==='HOY'||it.text==='ASAP' ? it.text+': '+it.nombre : it.text+' para '+it.nombre; return '
            '+ ''+it.icono+''+ ''+label+''+ ''+ '
            '; }).join(''); }; window._dismissDeadline=dismiss; /* Hook into existing data fetch — look for _dashData or retry */ function tryRender(){if(window._dashData){renderDeadlineBanners();}else{setTimeout(tryRender,500);}} tryRender(); /* Re-render on tab switch */ const origShowTab=window.showTab; if(origShowTab){window.showTab=function(t){origShowTab(t);renderDeadlineBanners();};}; })(); /* ═══ ENHANCED SEARCH: Keyboard navigation ═══ */ (function enhanceSearch(){ const searchInput=document.querySelector('#searchInput'); const searchResults=document.querySelector('#searchResults'); if(!searchInput||!searchResults) return; let activeIdx=-1; searchInput.addEventListener('keydown',function(e){ const items=searchResults.querySelectorAll('.search-result-item'); if(!items.length) return; if(e.key==='ArrowDown'){ e.preventDefault(); activeIdx=Math.min(activeIdx+1,items.length-1); items.forEach((el,i)=>el.classList.toggle('active',i===activeIdx)); items[activeIdx].scrollIntoView({block:'nearest'}); } else if(e.key==='ArrowUp'){ e.preventDefault(); activeIdx=Math.max(activeIdx-1,0); items.forEach((el,i)=>el.classList.toggle('active',i===activeIdx)); items[activeIdx].scrollIntoView({block:'nearest'}); } else if(e.key==='Enter'&&activeIdx>=0){ e.preventDefault(); items[activeIdx].click(); } }); searchInput.addEventListener('input',function(){activeIdx=-1;}); })(); /* ═══ COTIZADOR DE REMODELACIÓN ═══ */ (function initCotizador(){ const CONCEPTOS=[ {id:'pintura',nombre:'Pintura interior',unidad:'m²',min:80,max:150,icono:'🎨',defaultQty:129}, {id:'laminado',nombre:'Piso laminado',unidad:'m²',min:350,max:600,icono:'🪵',defaultQty:50}, {id:'ceramico',nombre:'Piso cerámico',unidad:'m²',min:400,max:800,icono:'🔲',defaultQty:25}, {id:'cocina',nombre:'Cocina integral',unidad:'pieza',min:25000,max:80000,icono:'🍳',defaultQty:1}, {id:'closet',nombre:'Closet de madera',unidad:'pieza',min:8000,max:25000,icono:'🚪',defaultQty:2}, {id:'impermeabilizacion',nombre:'Impermeabilización',unidad:'m²',min:100,max:200,icono:'💧',defaultQty:128}, {id:'electrica',nombre:'Instalación eléctrica',unidad:'punto',min:500,max:1200,icono:'⚡',defaultQty:10}, {id:'plomeria',nombre:'Plomería',unidad:'punto',min:800,max:2000,icono:'🔧',defaultQty:6}, {id:'limpieza',nombre:'Limpieza profunda',unidad:'servicio',min:2000,max:5000,icono:'🧹',defaultQty:1}, {id:'campana',nombre:'Campana/extractor',unidad:'pieza',min:3000,max:8000,icono:'🌬️',defaultQty:1} ]; const ROOMS=[ {nombre:'Sala/Comedor',m2:41}, {nombre:'Cocina',m2:12.2}, {nombre:'Oficina Art',m2:9}, {nombre:'Oficina Pame',m2:9}, {nombre:'Recámara',m2:10.5}, {nombre:'Baño completo',m2:7.5}, {nombre:'Baño pasillo',m2:6}, {nombre:'Cto. Servicio',m2:9}, {nombre:'Entrada/Pasillo',m2:18} ]; const LS_KEY='casita-cotizador'; let state=JSON.parse(localStorage.getItem(LS_KEY)||'null')||{}; function getState(id){ return state[id]||{checked:true,quality:'medio',qty:CONCEPTOS.find(c=>c.id===id).defaultQty}; } function saveState(){localStorage.setItem(LS_KEY,JSON.stringify(state));} function priceForQuality(c,q){ if(q==='basico') return c.min; if(q==='premium') return c.max; return Math.round((c.min+c.max)/2); } function render(){ const itemsEl=document.querySelector('#cotizador-items'); const totalsEl=document.querySelector('#cotizador-totals'); const roomEl=document.querySelector('#cotizador-room-breakdown'); if(!itemsEl) return; let totalMin=0,totalMid=0,totalMax=0; let html=''; CONCEPTOS.forEach(c=>{ const s=getState(c.id); const price=priceForQuality(c,s.quality); const subtotal=price*s.qty; if(s.checked){ totalMin+=c.min*s.qty; totalMid+=Math.round((c.min+c.max)/2)*s.qty; totalMax+=c.max*s.qty; } html+=`
            ${c.icono} ${c.nombre}
            $${subtotal.toLocaleString()}
            $${price.toLocaleString()}/${c.unidad}
            `; }); itemsEl.innerHTML=html; totalsEl.innerHTML=`
            $${totalMin.toLocaleString()}
            Mínimo
            $${totalMid.toLocaleString()}
            Medio
            $${totalMax.toLocaleString()}
            Premium
            `; // Room breakdown — pintura per room let roomHtml=''; const paintC=CONCEPTOS.find(c=>c.id==='pintura'); const paintS=getState('pintura'); const paintPrice=priceForQuality(paintC,paintS.quality); ROOMS.forEach(r=>{ const pinturaRoom=paintS.checked?Math.round(r.m2*paintPrice):0; roomHtml+=`
            ${r.nombre} ${r.m2}m²
            ~$${pinturaRoom.toLocaleString()}
            `; }); roomEl.innerHTML=roomHtml||'
            No hay conceptos seleccionados
            '; } window.cotizadorToggle=function(id,checked){state[id]=getState(id);state[id].checked=checked;saveState();render();}; window.cotizadorQuality=function(id,q){state[id]=getState(id);state[id].quality=q;saveState();render();}; window.cotizadorQty=function(id,v){state[id]=getState(id);state[id].qty=Math.max(0,parseInt(v)||0);saveState();render();}; window.cotizadorSelectAll=function(on){CONCEPTOS.forEach(c=>{state[c.id]=getState(c.id);state[c.id].checked=on;});saveState();render();}; window.cotizadorResetAll=function(){state={};localStorage.removeItem(LS_KEY);render();}; setTimeout(render,300); })(); // ═══ INVENTARIO V2 (Tab dedicado) ═══ const inv2Data = JSON.parse(localStorage.getItem('casita-inventario') || '[]'); const inv2CatIcons = {llevar:'🚚',comprar:'🛍️',vender:'💰',tirar:'🗑️'}; const inv2CatLabels = {llevar:'Llevar',comprar:'Comprar',vender:'Vender',tirar:'Tirar/Donar'}; const inv2RoomIcons = {'Sala/Comedor':'🛋️','Cocina':'🍳','Oficina Art':'💻','Oficina Pame':'💻','Recámara':'🛏️','Baños':'🚿','General':'🏠'}; function inv2Save() { localStorage.setItem('casita-inventario', JSON.stringify(inv2Data)); } function inv2Render() { const filterCuarto = $('#inv2-filter-cuarto').value; const filterCat = $('#inv2-filter-cat').value; const filterStatus = $('#inv2-filter-status').value; let filtered = inv2Data.filter(item => { if (filterCuarto && item.cuarto !== filterCuarto) return false; if (filterCat && item.categoria !== filterCat) return false; if (filterStatus && item.status !== filterStatus) return false; return true; }); // Group by room const rooms = {}; filtered.forEach(item => { if (!rooms[item.cuarto]) rooms[item.cuarto] = []; rooms[item.cuarto].push(item); }); const container = $('#inv2-list'); if (filtered.length === 0) { container.innerHTML = '
            📦 No hay items. Agrega algo al inventario.
            '; } else { let html = ''; const roomOrder = ['Sala/Comedor','Cocina','Oficina Art','Oficina Pame','Recámara','Baños','General']; roomOrder.forEach(room => { if (!rooms[room]) return; html += '
            '; html += '
            ' + (inv2RoomIcons[room]||'') + ' ' + room + ' (' + rooms[room].length + ')
            '; rooms[room].forEach(item => { const idx = inv2Data.indexOf(item); const priceStr = item.precio > 0 ? '$' + item.precio.toLocaleString() : ''; html += '
            '; html += ''; html += '' + item.nombre + ''; html += '' + inv2CatIcons[item.categoria] + ' ' + inv2CatLabels[item.categoria] + ''; if (priceStr) html += '' + priceStr + ''; html += ''; html += '
            '; }); html += '
            '; }); container.innerHTML = html; } inv2RenderSummary(); } function inv2RenderSummary() { const totalItems = inv2Data.length; const listos = inv2Data.filter(i => i.status === 'listo').length; const compras = inv2Data.filter(i => i.categoria === 'comprar').reduce((s, i) => s + (i.precio || 0), 0); const ventas = inv2Data.filter(i => i.categoria === 'vender').reduce((s, i) => s + (i.precio || 0), 0); const summary = $('#inv2-summary'); summary.innerHTML = [ {val: totalItems, lbl: 'Total items', color: 'var(--text)'}, {val: listos + '/' + totalItems, lbl: 'Listos', color: 'var(--green)'}, {val: '$' + compras.toLocaleString(), lbl: 'Por comprar', color: '#6495ed'}, {val: '$' + ventas.toLocaleString(), lbl: 'Por vender', color: 'var(--gold)'} ].map(c => '
            ' + c.val + '
            ' + c.lbl + '
            ').join(''); } function inv2Add() { const nombre = $('#inv2-nombre').value.trim(); if (!nombre) return; const item = { id: Date.now(), nombre, categoria: $('#inv2-categoria').value, cuarto: $('#inv2-cuarto').value, precio: parseInt($('#inv2-precio').value) || 0, status: 'pendiente' }; inv2Data.push(item); inv2Save(); $('#inv2-nombre').value = ''; $('#inv2-precio').value = ''; inv2Render(); } function inv2Toggle(idx) { inv2Data[idx].status = inv2Data[idx].status === 'listo' ? 'pendiente' : 'listo'; inv2Save(); inv2Render(); } function inv2Del(idx) { if (!confirm('¿Eliminar "' + inv2Data[idx].nombre + '"?')) return; inv2Data.splice(idx, 1); inv2Save(); inv2Render(); } // Enter key on nombre field document.getElementById('inv2-nombre').addEventListener('keydown', function(e) { if (e.key === 'Enter') inv2Add(); }); inv2Render(); // ═══ COMUNICACIÓN V2 (Tab dedicado) ═══ const comm2Data = JSON.parse(localStorage.getItem('casita-comunicacion') || '[]'); const comm2ContactIcons = {'BBVA':'🏦','Notaría 211':'🏛️','Administración':'🏢','Prosoc':'⚖️','Vecino':'👤','Otro':'📋'}; const comm2TypeLabels = {llamada:'📞 Llamada',email:'📧 Email',presencial:'🧑‍💼 Presencial',whatsapp:'📱 WhatsApp'}; const comm2StatusLabels = {informativo:'ℹ️ Informativo',pendiente:'⏳ Pendiente',resuelto:'✅ Resuelto'}; (function initComm2Date() { const d = new Date(); const el = document.getElementById('comm2-fecha'); if (el) el.value = d.toISOString().split('T')[0]; })(); function comm2Save() { localStorage.setItem('casita-comunicacion', JSON.stringify(comm2Data)); } function comm2UpdateBadge() { const pending = comm2Data.filter(e => e.status === 'pendiente').length; const badge = document.getElementById('comm2-badge'); if (badge) badge.textContent = pending > 0 ? pending + ' pendiente' + (pending > 1 ? 's' : '') : ''; // Also update tab label const tabEl = document.getElementById('tab-comunicacion'); if (tabEl) { tabEl.textContent = '📞 Comunicación' + (pending > 0 ? ' (' + pending + ')' : ''); } } function comm2Render() { const filterContacto = $('#comm2-filter-contacto').value; const filterStatus = $('#comm2-filter-status').value; let filtered = comm2Data.filter(entry => { if (filterContacto && entry.contacto !== filterContacto) return false; if (filterStatus && entry.status !== filterStatus) return false; return true; }); // Sort by date descending filtered.sort((a, b) => new Date(b.fecha) - new Date(a.fecha)); const container = $('#comm2-timeline'); if (filtered.length === 0) { container.innerHTML = '
            📞 No hay comunicaciones registradas.
            '; } else { let html = ''; filtered.forEach(entry => { const idx = comm2Data.indexOf(entry); const icon = comm2ContactIcons[entry.contacto] || '📋'; html += '
            '; html += '
            ' + icon + '
            '; html += '
            '; html += '
            '; html += '' + entry.contacto + ''; html += '' + (comm2TypeLabels[entry.tipo] || entry.tipo) + ''; html += '' + (comm2StatusLabels[entry.status] || entry.status) + ''; html += ''; html += '
            '; html += '
            ' + entry.resumen + '
            '; if (entry.accion) html += '
            ➡ ' + entry.accion + '
            '; html += '
            '; html += '
            '; if (entry.status === 'pendiente') html += ''; html += ''; html += '
            '; html += '
            '; }); container.innerHTML = html; } comm2UpdateBadge(); } function comm2Add() { const resumen = $('#comm2-resumen').value.trim(); if (!resumen) return; const entry = { id: Date.now(), fecha: $('#comm2-fecha').value, contacto: $('#comm2-contacto').value, tipo: $('#comm2-tipo').value, resumen, accion: $('#comm2-accion').value.trim(), status: $('#comm2-status').value }; comm2Data.push(entry); comm2Save(); $('#comm2-resumen').value = ''; $('#comm2-accion').value = ''; comm2Render(); } function comm2Resolve(idx) { comm2Data[idx].status = 'resuelto'; comm2Save(); comm2Render(); } function comm2Del(idx) { if (!confirm('¿Eliminar esta comunicación?')) return; comm2Data.splice(idx, 1); comm2Save(); comm2Render(); } comm2Render(); /* ═══ CHANGELOG / NOVEDADES ═══ */ (function initChangelog(){ var SEEN_KEY='casita_changelog_seen'; var DEFAULT_VISIBLE=3; var items=[ {icon:'💚',text:'Health Score del depa',date:'2026-04-14'}, {icon:'🛒',text:'Lista de compras inteligente',date:'2026-04-13'}, {icon:'💰',text:'Cotizador de materiales',date:'2026-04-12'}, {icon:'⏰',text:'Deadline banners de urgencia',date:'2026-04-12'}, {icon:'🔗',text:'Modo compartir (share)',date:'2026-04-11'}, {icon:'🔐',text:'Autenticación con contraseña',date:'2026-04-10'}, {icon:'🎯',text:'Ruta Crítica con dependencias',date:'2026-04-09'}, {icon:'📄',text:'Exportar PDF corregido',date:'2026-04-08'}, {icon:'📱',text:'PWA — instalar como app',date:'2026-04-06'}, {icon:'🌙',text:'Modo claro / oscuro',date:'2026-04-05'} ]; var expanded=false; function relativeDate(dateStr){ var today=new Date();today.setHours(0,0,0,0); var target=new Date(dateStr+'T00:00:00');target.setHours(0,0,0,0); var diff=Math.round((today-target)/(1000*60*60*24)); if(diff===0) return 'hoy'; if(diff===1) return 'ayer'; if(diff<7) return 'hace '+diff+' días'; var weeks=Math.floor(diff/7); if(weeks===1) return 'hace 1 semana'; if(diff<30) return 'hace '+weeks+' semanas'; var months=Math.floor(diff/30); return 'hace '+months+(months===1?' mes':' meses'); } function hasNew(){ var seen=localStorage.getItem(SEEN_KEY); if(!seen) return true; return items[0].date>seen; } function markSeen(){ localStorage.setItem(SEEN_KEY,items[0].date); } function render(){ var visible=expanded?items:items.slice(0,DEFAULT_VISIBLE); var html=''; visible.forEach(function(it){ html+='
            '; html+=''+it.icon+''; html+=''+it.text+''; html+=''+relativeDate(it.date)+''; html+='
            '; }); if(items.length>DEFAULT_VISIBLE){ html+=''; } $('#changelog-list').innerHTML=html; var toggle=document.getElementById('changelog-toggle'); if(toggle){toggle.addEventListener('click',function(){expanded=!expanded;render();});} } function renderSummary(){ var dot=hasNew()?'':''; $('#changelog-summary').innerHTML='🆕 Novedades ('+items.length+')'+dot; } var details=$('#changelog-details'); details.addEventListener('toggle',function(){ if(details.open){ markSeen(); renderSummary(); } }); render(); renderSummary(); })(); /* ═══ MODO POST-FIRMA ═══ */ (function initPostFirma(){ function detectPhase(){ const statuses=JSON.parse(localStorage.getItem('casita-ruta-critica')||'{}'); if(statuses.mudanza==='listo') return 'post-mudanza'; if(statuses.entrega==='listo') return 'remodelacion'; if(statuses.firma==='listo') return 'post-firma'; return 'tramite'; } function applyPhase(){ const phase=detectPhase(); document.body.dataset.casitaPhase=phase; // Update banner content based on phase const icon=$('#phase-icon'); const title=$('#phase-title'); const sub=$('#phase-sub'); if(!icon) return; if(phase==='post-firma'){ icon.textContent='✍️'; title.textContent='¡Escrituras firmadas!'; title.style.color='var(--green)'; sub.textContent='El depa es suyo. Siguiente: entrega de llaves.'; } else if(phase==='remodelacion'){ icon.textContent='🔑'; title.textContent='¡Tienen las llaves!'; title.style.color='var(--gold)'; sub.textContent='Hora de remodelar y preparar la mudanza.'; } else if(phase==='post-mudanza'){ icon.textContent='🏠'; title.textContent='¡Bienvenidos a casa!'; title.style.color='var(--gold)'; sub.textContent='Ya viven aquí. Casita ahora es su hogar.'; } // Override KPIs in post-firma modes if(phase!=='tramite'){ const kpis=$('#kpis-container'); if(kpis){ const statuses=JSON.parse(localStorage.getItem('casita-ruta-critica')||'{}'); const rutaData=window.__CASITA_DATA&&window.__CASITA_DATA.rutaCritica?window.__CASITA_DATA.rutaCritica.pasos:[]; const done=rutaData.filter(p=>(statuses[p.id]||p.status)==='listo').length; const total=rutaData.length||9; const pct=Math.round(done/total*100); // Count inventory items const inv=JSON.parse(localStorage.getItem('casita-inventario')||'[]'); const invDone=inv.filter(i=>i.status==='listo').length; // Countdown to estimated move date const notif=window.__CASITA_DATA&&window.__CASITA_DATA.notificaciones||{}; const moveDateStr=notif.entregaDate||'2026-07-15'; const moveDate=new Date(moveDateStr+'T00:00:00'); const today=new Date();today.setHours(0,0,0,0); const daysToMove=Math.max(0,Math.round((moveDate-today)/(1000*60*60*24))); const newKpis=[ {valor:'✅',label:'Escrituras',sub:'Firmadas',color:'green',estilo:'var(--green)'}, {valor:pct+'%',label:'Ruta Crítica',sub:done+'/'+total+' pasos',color:'green',estilo:'var(--green)'}, {valor:daysToMove+'d',label:'Mudanza',sub:'~julio 2026',color:'amber',estilo:'var(--amber)'}, {valor:inv.length>0?invDone+'/'+inv.length:'0',label:'Inventario',sub:inv.length>0?'items listos':'sin items',color:'amber',estilo:'var(--amber)'} ]; kpis.innerHTML=newKpis.map(k=>'
            '+k.valor+'
            '+k.label+'
            '+k.sub+'
            ').join(''); } // Override digest const dig=$('#digest-container'); if(dig){ if(phase==='post-firma') dig.innerHTML='🏠 ¡Firmaron! Siguiente paso: entrega de llaves. Preparen mudanza e inventario.'; else if(phase==='remodelacion') dig.innerHTML='🔧 Remodelación en curso. Revisa el cotizador y el inventario de mudanza.'; else if(phase==='post-mudanza') dig.innerHTML='🎉 ¡Ya viven en Narvarte! Casita ahora es su hogar.'; } } } // Apply on load and on localStorage changes setTimeout(applyPhase,400); window.addEventListener('storage',function(e){ if(e.key==='casita-ruta-critica') applyPhase(); }); // Re-apply after ruta critica renders (it saves to localStorage) const origRender=window.renderRutaCritica; if(origRender){ window.renderRutaCritica=function(){ origRender(); setTimeout(applyPhase,100); }; } })();