Files
IDE-Demo-B/js/main.js
T
2026-10-07 11:37:16 -03:00

1345 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* IDE CONSULTORES - SCRIPT INSTITUCIONAL (JS ÚNICO)
* Maneja includes modulares, tema claro/oscuro, Brújula IDE, modales, catálogo de cursos, filtros y formularios.
*/
document.addEventListener('DOMContentLoaded', () => {
initApp();
});
async function initApp() {
initTheme();
initHeroVideoSpeed();
await loadIncludes();
initModals();
initAcademyCourses();
initSolucionesFilters();
initInsightsFeed();
initContactForm();
initCareersForm();
initSearchPage();
}
/* ==========================================================================
1. Carga Modular de Header y Footer (JS Nativo)
========================================================================== */
async function loadIncludes() {
const headerElem = document.getElementById('site-header');
const footerElem = document.getElementById('site-footer');
try {
if (headerElem && headerElem.children.length === 0) {
const headerRes = await fetch('./includes/header.html', { cache: 'no-store' });
if (headerRes.ok) {
headerElem.innerHTML = await headerRes.text();
highlightActiveNavLink();
attachHeaderEvents();
} else {
// ← NUEVO: Si el fetch falla (ej: 404), igual inicializar eventos
highlightActiveNavLink();
attachHeaderEvents();
}
} else if (headerElem) {
highlightActiveNavLink();
attachHeaderEvents();
}
if (footerElem) {
const footerRes = await fetch('./includes/footer.html', { cache: 'no-store' });
if (footerRes.ok) {
footerElem.innerHTML = await footerRes.text();
}
}
} catch (err) {
console.warn('Header modular no disponible:', err);
// ← NUEVO: Intentar inicializar igual si el header ya tiene contenido estático
if (headerElem && headerElem.children.length > 0) {
highlightActiveNavLink();
attachHeaderEvents();
}
}
}
function highlightActiveNavLink() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.nav-link');
// 1. Limpiar estado activo previo
navLinks.forEach(link => link.classList.remove('active'));
// 2. Evaluar cada enlace
navLinks.forEach(link => {
const href = link.getAttribute('href');
// Ignoramos enlaces vacíos o anclas "#"
if (!href || href === '#') return;
const isHome = (currentPath === '/' || currentPath.endsWith('index.html')) &&
(href === '/' || href === 'index.html' || href === './');
const isCurrentPage = currentPath.includes(href) && href !== '/' && href !== 'index.html';
if (isHome || isCurrentPage) {
link.classList.add('active');
// Si este enlace está dentro de un submenú, marcar también al padre
const parentDropdown = link.closest('.dropdown');
if (parentDropdown) {
const parentToggle = parentDropdown.querySelector('.dropdown-toggle');
if (parentToggle) {
parentToggle.classList.add('active');
}
}
}
});
}
function attachHeaderEvents() {
// Theme toggle
const themeToggle = document.getElementById('theme-toggle-btn');
if (themeToggle) {
const newThemeToggle = themeToggle.cloneNode(true);
themeToggle.parentNode.replaceChild(newThemeToggle, themeToggle);
newThemeToggle.addEventListener('click', toggleTheme);
}
const drawer = document.getElementById('mobile-menu-drawer');
const overlay = document.getElementById('mobile-menu-overlay');
function openDrawer() {
if (!drawer) return;
drawer.classList.add('is-open');
drawer.setAttribute('aria-hidden', 'false');
if (overlay) overlay.classList.add('is-open');
document.body.style.overflow = 'hidden'; // evita scroll del fondo
const btn = document.getElementById('mobile-menu-btn');
if (btn) btn.setAttribute('aria-expanded', 'true');
}
function closeDrawer() {
if (!drawer) return;
drawer.classList.remove('is-open');
drawer.setAttribute('aria-hidden', 'true');
if (overlay) overlay.classList.remove('is-open');
document.body.style.overflow = '';
const btn = document.getElementById('mobile-menu-btn');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
// Botón hamburguesa abre
document.getElementById('mobile-menu-btn')
?.addEventListener('click', openDrawer);
// Botón X dentro del drawer cierra
document.getElementById('drawer-close-btn')
?.addEventListener('click', closeDrawer);
// Clic en overlay cierra
if (overlay) overlay.addEventListener('click', closeDrawer);
// Clic en cualquier link del drawer cierra
drawer?.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', closeDrawer);
});
// Tecla ESC cierra
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDrawer();
});
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
toggleLogoTheme(currentTheme);
updateThemeIcon(currentTheme);
}
/* ==========================================================================
2. Selector de Modo Claro / Modo Oscuro con Persistencia
========================================================================== */
function initTheme() {
const savedTheme = localStorage.getItem('ide_theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
toggleLogoTheme(savedTheme);
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('ide_theme', newTheme);
updateThemeIcon(newTheme);
toggleLogoTheme(newTheme);
const themeBtns = document.querySelectorAll('#theme-toggle-btn');
themeBtns.forEach(btn => {
btn.setAttribute('aria-label', newTheme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro');
});
}
function updateThemeIcon(theme) {
const themeBtns = document.querySelectorAll('#theme-toggle-btn');
themeBtns.forEach(btn => {
btn.setAttribute('aria-label', theme === 'dark' ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro');
});
}
function toggleLogoTheme(theme) {
const logoLight = document.querySelector('.logo-light-only');
const logoDark = document.querySelector('.logo-dark-only');
if (logoLight && logoDark) {
if (theme === 'dark') {
logoLight.style.display = 'none';
logoDark.style.display = 'block';
} else {
logoLight.style.display = 'block';
logoDark.style.display = 'none';
}
}
}
/* ==========================================================================
4. Sistema de Modales (Pop-ups con Cierre por Clic Externo y ESC)
========================================================================== */
function initModals() {
const modalBackdrops = document.querySelectorAll('.modal-backdrop');
modalBackdrops.forEach(backdrop => {
// Cerrar al hacer clic en el backdrop (fuera de la ventana)
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
closeModal(backdrop.id);
}
});
// Botones de cierre interno
const closeBtns = backdrop.querySelectorAll('.modal-close-btn, [data-close-modal]');
closeBtns.forEach(btn => {
btn.addEventListener('click', () => {
closeModal(backdrop.id);
});
});
});
// Cerrar con tecla ESC
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const activeModal = document.querySelector('.modal-backdrop.is-active');
if (activeModal) {
closeModal(activeModal.id);
}
}
});
// Triggers de modales genéricos en páginas (ej: desafíos)
document.querySelectorAll('[data-open-modal]').forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
const modalId = trigger.getAttribute('data-open-modal');
openModal(modalId);
});
});
}
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('is-active');
document.body.style.overflow = 'hidden'; // Evita scroll de fondo
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('is-active');
document.body.style.overflow = '';
}
}
/* ================================================================
JS QUIZ
================================================================ */
(function () {
// ----- FLUJO DE NAVEGACIÓN (Opción B) -----
const FLOW = {
1: { desafio: '2a', objetivo: '2b', solucion: '2c', conocimiento: '2d' },
'2a': 3, '2b': 3, '2c': 3, '2d': 3,
3: 4,
4: 'result'
};
// ----- MAPEO DE RESULTADOS (Opción B) -----
function buildResult(answers) {
const p1 = answers[1];
const p2 = answers['2a'] || answers['2b'] || answers['2c'] || answers['2d'];
const p3 = answers[3];
// Perfil 4 · Requisito exigido
if (p3 === 'requisito' || p2 === 'iso' || p2 === 'iso-form') {
return {
title: 'La certificación puede ser el punto de partida.',
body: 'Comprendemos que tenés un requisito externo. En IDE no trabajamos para cumplir auditorías: trabajamos para que el sistema refleje cómo funciona tu organización y contribuya a gestionarla mejor.',
chips: ['IDE Excellence', 'ISO 9001 / 14001 / 45001', 'Sistemas de gestión'],
cta: 'Ver soluciones Excellence',
url: 'soluciones.html'
};
}
// Perfil 5 · Conocimiento o formación
if (p1 === 'conocimiento') {
const map = {
'iso-form': { chips: ['Auditoría interna', 'ISO 9001', 'Mejora continua'], extra: 'IDE Excellence' },
'pm-form': { chips: ['Gestión de proyectos', 'Metodologías ágiles', 'PMP'], extra: 'IDE Project' },
'liderazgo-form': { chips: ['Liderazgo', 'Cambio organizacional', 'Cultura'], extra: 'IDE Academy' },
'ia-form': { chips: ['IA aplicada', 'Transformación digital', 'Automatización'], extra: 'IDE Digital' }
};
const m = map[p2] || { chips: ['Formación', 'Desarrollo', 'Academy'], extra: 'IDE Academy' };
return {
title: 'Desarrollamos las capacidades que hacen posible la transformación.',
body: 'IDE Academy diseña programas de formación alineados con los desafíos reales de las organizaciones. No dictamos cursos estándar: trabajamos desde el contexto de cada equipo.',
chips: [...m.chips, m.extra],
cta: 'Conocer IDE Academy',
url: 'academy.html'
};
}
// Perfil 3 · Sabe exactamente qué necesita
if (p1 === 'solucion') {
const map = {
iso: {
title: 'Respuesta concreta. Y algo más.',
chips: ['ISO 9001', 'ISO 14001', 'IDE Excellence'],
cta: 'Ver soluciones ISO',
url: 'soluciones.html',
body: 'Sí, implementamos y acompañamos certificaciones ISO. Y creemos que un sistema de gestión debe reflejar cómo funciona tu organización, no solo superar una auditoría.'
},
pmo: {
title: 'Proyectos que llegan a destino.',
chips: ['PMO', 'Gestión de proyectos', 'IDE Project'],
cta: 'Ver soluciones Project',
url: 'soluciones.html',
body: 'Diseñamos PMOs y acompañamos la dirección de proyectos para que se entreguen en tiempo, costo y con los resultados esperados.'
},
formacion: {
title: 'Formación desde el contexto real.',
chips: ['In Company', 'Capacitación', 'IDE Academy'],
cta: 'Ver IDE Academy',
url: 'academy.html',
body: 'Programas de formación diseñados para tu organización, sus desafíos y las capacidades que necesita desarrollar.'
},
'ia-digital': {
title: 'Tecnología con propósito.',
chips: ['Transformación digital', 'IA aplicada', 'IDE Digital'],
cta: 'Ver soluciones Digital',
url: 'soluciones.html',
body: 'Te ayudamos a identificar dónde la IA y la digitalización generan valor real, y acompañamos su implementación de principio a fin.'
}
};
const m = map[p2] || {
title: 'Encontramos lo que buscás.',
chips: ['Soluciones concretas', 'Ecosistema IDE'],
cta: 'Ver soluciones',
url: 'soluciones.html',
body: 'Tenemos la respuesta concreta que buscás. Y una mirada que asegura que esa solución genere valor en el conjunto de tu organización.'
};
return { icon: './assets/iconos/engranaje_32.svg', profile: 'Perfil · Solución concreta', ...m };
}
// Perfil 2 · Sabe qué quiere lograr
if (p1 === 'objetivo') {
const map = {
'mejorar-gestion': { chips: ['IDE Excellence', 'Mejora de procesos'], body: 'Entendemos dónde querés llegar. Antes de proponer una solución, comprendemos tu operación para intervenir donde hay mayor impacto.' },
'profesionalizar-proyectos':{ chips: ['IDE Project', 'PMO'], body: 'Traducimos tu objetivo en una estructura de gestión de proyectos adecuada a tu contexto, sin sobreingeniería.' },
'desarrollar-equipo': { chips: ['IDE Academy', 'Formación In Company'], body: 'Desarrollamos capacidades alineadas con los desafíos reales de tu organización. La formación parte del contexto, no de un catálogo estándar.' },
'transformar-digital': { chips: ['IDE Digital', 'IA aplicada'], body: 'Empezamos por entender tu organización para identificar dónde la tecnología realmente agrega valor, y avanzamos desde ahí.' }
};
const m = map[p2] || { chips: ['Ecosistema IDE'], body: 'Entendemos dónde querés llegar. Comprendemos el sistema para ayudarte a lograrlo.' };
return {
title: 'Entendemos dónde querés llegar. Te ayudamos a definir cómo.',
body: m.body,
chips: m.chips,
cta: 'Explorar por desafío',
url: 'desafios.html'
};
}
// Perfil 1 · Desafío sin solución definida
const mapD = {
gestion: { chips: ['Procesos', 'Gestión', 'IDE Excellence'] },
proyectos: { chips: ['Proyectos', 'PMO', 'IDE Project'] },
personas: { chips: ['Capacidades', 'Equipo', 'IDE Academy'] },
tecnologia: { chips: ['IA', 'Digital', 'IDE Digital'] },
nosabe: { chips: ['Diagnóstico sistémico', 'Ecosistema IDE'] }
};
const md = mapD[p2] || mapD.nosabe;
return {
title: 'No necesitás tener la respuesta para empezar.',
body: 'Empezamos por comprender tu situación, no por ofrecerte una solución predeterminada. Eso diferencia una intervención puntual de una transformación real.',
chips: md.chips,
cta: 'Explorar desafíos',
url: 'desafios.html'
};
}
// ----- ESTADO -----
let currentStep = 1;
let stepHistory = [];
let answers = {};
const TOTAL = 4;
const container = document.getElementById('quiz-interactive-app');
if (!container) return;
// ----- FUNCIONES -----
function showStep(id) {
const steps = container.querySelectorAll('.quiz-step');
steps.forEach(s => s.classList.remove('active'));
const el = document.getElementById('step-' + id);
if (el) el.classList.add('active');
updateProgress(id);
updateIndicator(id);
}
function updateProgress(id) {
const fill = document.getElementById('quiz-progress-fill');
if (!fill) return;
let n = 1;
if (id === 'result') n = 4;
else if (['2a', '2b', '2c', '2d'].includes(String(id))) n = 2;
else n = parseInt(id) || 1;
fill.style.width = (n / TOTAL * 100) + '%';
}
function updateIndicator(id) {
const label = document.getElementById('quiz-step-indicator');
if (!label) return;
let text = '';
if (id === 'result') text = '¡Orientación lista!';
else if (['2a', '2b', '2c', '2d'].includes(String(id))) text = 'Paso 2 de 4';
else text = 'Paso ' + id + ' de 4';
label.textContent = text;
}
function renderResult(ans) {
const r = buildResult(ans);
document.getElementById('resultTitle').textContent = r.title;
document.getElementById('resultBody').textContent = r.body;
const chipsContainer = document.getElementById('resultChips');
chipsContainer.innerHTML = r.chips.map(c =>
`<span class="result-chip">${c}</span>`
).join('');
const cta = document.getElementById('resultCTA');
cta.textContent = r.cta;
cta.href = r.url;
showStep('result');
}
function handleOption(btn) {
const step = btn.dataset.step;
const val = btn.dataset.val;
// Deseleccionar otras opciones del mismo paso
const parent = btn.closest('.quiz-options-grid');
if (parent) {
parent.querySelectorAll('.quiz-option-card').forEach(b => b.classList.remove('selected'));
}
btn.classList.add('selected');
answers[step] = val;
stepHistory.push(step);
const next = FLOW[step];
if (!next) return;
if (next === 'result') {
renderResult(answers);
return;
}
const nextId = typeof next === 'object' ? (next[val] || '2a') : next;
showStep(nextId);
}
// ----- FUNCIONES GLOBALES (para onclick en HTML) -----
window.goBack = function () {
if (!stepHistory.length) return;
const prev = stepHistory.pop();
delete answers[prev];
const last = stepHistory[stepHistory.length - 1] || 1;
showStep(last);
};
window.restartQuiz = function () {
answers = {};
stepHistory = [];
container.querySelectorAll('.quiz-option-card').forEach(b => b.classList.remove('selected'));
showStep(1);
};
// ----- INIT -----
function initQuiz() {
// Eventos en las cards de opción
container.querySelectorAll('.quiz-option-card').forEach(btn => {
btn.addEventListener('click', function () {
handleOption(this);
});
});
showStep(1);
}
// Ejecutar al cargar el DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initQuiz);
} else {
initQuiz();
}
})();
/* ==========================================================================
6. Carga Dinámica de Cursos en IDE Academy (cursos.json)
========================================================================== */
const GOOGLE_SHEETS_API_URL = 'https://script.google.com/macros/s/AKfycbzsZUvY29i9w0SEPYqL46SJSr8AqG0kBxKP5-RFJmr2ABd6JaF9L5o4iUhOYUwrDiYn/exec';
let allCourses = [];
async function initAcademyCourses() {
const container = document.getElementById('academy-courses-container');
if (!container) return;
try {
// 1. Revisar si tenemos datos cacheados en la sesión para carga ultra rápida
const cachedData = sessionStorage.getItem('ide_academy_courses');
if (cachedData) {
allCourses = filterVisibleCourses(JSON.parse(cachedData));
renderCourseCards(allCourses, container);
}
// 2. Traer la información fresca desde Google Sheets
const res = await fetch(GOOGLE_SHEETS_API_URL);
if (!res.ok) throw new Error('Error al conectar con Google Sheets');
const freshData = await res.json();
// Guardar en caché de sesión
sessionStorage.setItem('ide_academy_courses', JSON.stringify(freshData));
// Filtrar y renderizar
allCourses = filterVisibleCourses(freshData);
renderCourseCards(allCourses, container);
} catch (err) {
console.warn('Error con Google Sheets, recurriendo a respaldo:', err);
// Si falla la red o Google, usamos el fallback
if (allCourses.length === 0) {
allCourses = filterVisibleCourses(getFallbackCourses());
renderCourseCards(allCourses, container);
}
}
}
/**
* Filtra cursos por estado: solo admite "Publicado" y "Próximamente"
* Normaliza acentos y mayúsculas/minúsculas para evitar errores tipográficos
*/
function filterVisibleCourses(courses) {
if (!Array.isArray(courses)) return [];
return courses.filter(course => {
if (!course.estado) return false;
const cleanStatus = course.estado
.trim()
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, ""); // "próximamente" -> "proximamente"
return cleanStatus === 'publicado' || cleanStatus === 'proximamente';
});
}
function renderCourseCards(courses, container) {
container.innerHTML = '';
if (courses.length === 0) {
container.innerHTML = `
<div class="card text-center" style="grid-column: 1 / -1; padding: 2.5rem;">
<p style="color: var(--text-muted); font-size: 1rem;">
No hay convocatorias abiertas en este momento. Consultanos por programas a medida o In-Company.
</p>
</div>
`;
return;
}
courses.forEach(course => {
const card = document.createElement('div');
card.className = 'card';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.justifyContent = 'space-between';
const area = course.area_tematica || course.categoria || 'IDE Academy';
const tipo = course.tipo ? ` • ${course.tipo}` : '';
const proxima = course.proxima_edicion || course.proxima_fecha || 'A confirmar';
const duracion = course.duracion || 'A definir';
const modalidad = course.modalidad || 'Consultar';
const nivel = course.nivel || 'General';
const descripcion = course.propuesta_formativa || course.descripcion_corta || '';
// Estilo distintivo sutil si está en "Próximamente"
const isProximamente = course.estado.toLowerCase().includes('prox');
const badgeProximo = isProximamente
? `<span style="font-size: 0.72rem; background: var(--bg-subtle); padding: 0.2rem 0.5rem; border-radius: 4px; color: #D97706; font-weight: 600; border: 1px solid rgba(217, 119, 6, 0.2);">Próximamente</span>`
: '';
card.innerHTML = `
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; gap: 0.5rem; flex-wrap: wrap;">
<div class="ecosystem-badge badge-academy">${area}${tipo}</div>
${badgeProximo}
</div>
<h3 style="font-size: 1.15rem; margin-bottom: 0.6rem; line-height: 1.35;">${course.titulo}</h3>
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 1.25rem; line-height: 1.5;">${descripcion}</p>
</div>
<div>
<div style="font-size: 0.82rem; color: var(--text-body); display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1.5rem; border-top: 1px solid var(--border-color); padding-top: 0.85rem;">
<div><strong>Modalidad:</strong> ${modalidad}</div>
<div><strong>Duración:</strong> ${duracion}</div>
<div><strong>Nivel:</strong> ${nivel}</div>
<div><strong>Próxima edición:</strong> <span style="${proxima.toLowerCase() === 'a confirmar' ? 'color: #D97706; font-weight: 600;' : ''}">${proxima}</span></div>
</div>
<button class="btn btn-outline btn-sm w-full" style="width: 100%;" onclick="openCourseModal('${course.id}')">
Ver Ficha Técnica y Programa
</button>
</div>
`;
container.appendChild(card);
});
}
// Helper para convertir tanto arrays como textos planos con viñetas en <li> HTML
function renderListItems(data) {
if (!data) return '<li>A consultar con el equipo académico.</li>';
if (Array.isArray(data)) {
return data.map(item => `<li>${item}</li>`).join('');
}
if (typeof data === 'string') {
return data
.split('\n')
.map(line => line.replace(/^[•\-\*\d+\.\s]+/, '').trim())
.filter(line => line.length > 0)
.map(line => `<li>${line}</li>`)
.join('');
}
return `<li>${data}</li>`;
}
// Modal dinámico de cursos adaptado a la ficha de administración
window.openCourseModal = function (courseId) {
const course = allCourses.find(c => c.id === courseId);
if (!course) return;
const titleElem = document.getElementById('modal-course-title');
const catElem = document.getElementById('modal-course-category');
const detailsElem = document.getElementById('modal-course-details');
const ctaElem = document.getElementById('modal-course-cta');
if (titleElem) titleElem.innerText = course.titulo;
const area = course.area_tematica || course.categoria || 'IDE Academy';
const tipo = course.tipo || 'Curso';
const modalidad = course.modalidad || 'Online';
const duracion = course.duracion || 'A definir';
const nivel = course.nivel || 'Todos los niveles';
const proxima = course.proxima_edicion || course.proxima_fecha || 'A confirmar';
if (catElem) {
catElem.innerHTML = `
<div class="course-header-top" style="margin-bottom: 0.8rem; display: flex; align-items: center; gap: 0.5rem;">
<span class="course-badge-category" style="text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.05em; font-weight: 700;">
${area} • ${tipo}
</span>
<span style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">[${course.id}]</span>
</div>
<div class="course-specs-panel">
<!-- Modalidad -->
<div class="spec-item">
<div class="spec-icon-box spec-icon-blue">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</div>
<div class="spec-text">
<span class="spec-label">Modalidad</span>
<strong class="spec-value">${modalidad}</strong>
</div>
</div>
<!-- Duración -->
<div class="spec-item">
<div class="spec-icon-box spec-icon-purple">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
</div>
<div class="spec-text">
<span class="spec-label">Duración</span>
<strong class="spec-value">${duracion}</strong>
</div>
</div>
<!-- Nivel (Reemplaza Sector) -->
<div class="spec-item">
<div class="spec-icon-box spec-icon-teal">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
</div>
<div class="spec-text">
<span class="spec-label">Nivel</span>
<strong class="spec-value">${nivel}</strong>
</div>
</div>
<!-- Próxima Edición -->
<div class="spec-item">
<div class="spec-icon-box spec-icon-amber">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</div>
<div class="spec-text">
<span class="spec-label">Próxima edición</span>
<strong class="spec-value highlight-date" style="${proxima.toLowerCase() === 'a confirmar' ? 'color: #D97706;' : ''}">
${proxima}
</strong>
</div>
</div>
</div>
`;
}
if (detailsElem) {
const objetivosList = course.que_vas_a_desarrollar || course.objetivos;
const temarioList = course.recorrido_aprendizaje || course.temario;
const propuesta = course.propuesta_formativa || course.propuesta || '';
const destinatarios = course.destinatarios || 'Profesionales interesados en la materia.';
const docente = course.equipo_docente || course.docente || 'Equipo IDE Consultores';
const comercial = course.informacion_comercial || course.resultados_esperados || 'Disponible para formato abierto o In-Company.';
detailsElem.innerHTML = `
<!-- Propuesta Formativa -->
<div style="margin-bottom: 1.5rem;">
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Propuesta Formativa</h4>
<p style="font-size: 0.92rem; color: var(--text-body); line-height: 1.55;">${propuesta}</p>
</div>
<!-- Destinatarios (Nuevo campo) -->
<div style="margin-bottom: 1.5rem; background: rgba(0,0,0,0.02); padding: 0.85rem; border-left: 3px solid var(--ide-primary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<h4 style="font-size: 0.92rem; color: var(--ide-primary); margin-bottom: 0.25rem;">Destinatarios</h4>
<p style="font-size: 0.88rem; color: var(--text-body); margin: 0;">${destinatarios}</p>
</div>
<!-- Qué vas a desarrollar -->
<div style="margin-bottom: 1.5rem;">
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Qué vas a desarrollar</h4>
<ul style="padding-left: 1.25rem; font-size: 0.9rem; color: var(--text-body); line-height: 1.6;">
${renderListItems(objetivosList)}
</ul>
</div>
<!-- Recorrido de aprendizaje -->
<div style="margin-bottom: 1.5rem;">
<h4 style="font-size: 1rem; color: var(--ide-primary); margin-bottom: 0.4rem;">Recorrido de Aprendizaje</h4>
<ul style="padding-left: 1.25rem; font-size: 0.9rem; color: var(--text-body); line-height: 1.6; list-style-type: decimal;">
${renderListItems(temarioList)}
</ul>
</div>
<!-- Docente e Info Comercial -->
<div style="background-color: var(--bg-subtle); padding: 1.1rem; border-radius: var(--radius-sm); font-size: 0.88rem; margin-bottom: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem;">
<div><strong>Equipo Docente:</strong> ${docente}</div>
<div><strong>Información y Condiciones:</strong> ${comercial}</div>
</div>
`;
}
if (ctaElem) {
const params = new URLSearchParams({
origen: 'IDE_Academy',
busqueda: 'Formación para mi organización',
curso: `${course.titulo} (${course.id})`
});
ctaElem.setAttribute('href', `contacto.html?${params.toString()}`);
}
openModal('modal-course-view');
};
function getFallbackCourses() {
return [
{
id: "ACA-GC-001",
estado: "Próximamente",
titulo: "Calidad que genera valor: De la norma a la gestión real",
area_tematica: "Gestión & Calidad",
tipo: "Curso",
modalidad: "Online sincrónico",
duracion: "12 horas – 4 encuentros",
nivel: "Intermedio",
destinatarios: "Responsables de calidad, líderes de procesos y mandos medios.",
propuesta_formativa: "Una experiencia práctica y estratégica para transformar el sistema de calidad en una herramienta real de gestión.",
que_vas_a_desarrollar: [
"Capacidad para diagnosticar cuellos de botella reales.",
"Diseño de indicadores con impacto operativo.",
"Liderazgo en auditorías con enfoque de valor."
],
recorrido_aprendizaje: [
"1. De cumplir normas a gestionar resultados.",
"2. Mapeo y rediseño de procesos críticos.",
"3. Indicadores clave y tableros de gestión.",
"4. Auditoría interna con valor agregado."
],
equipo_docente: "Equipo IDE Excellence",
proxima_edicion: "A confirmar",
informacion_comercial: "Próxima edición con cupos limitados. Consultar por ediciones In-Company."
}
];
}
/* ==========================================================================
7. Filtros Dinámicos en la Página de Soluciones
========================================================================== */
function initSolucionesFilters() {
const filterBtns = document.querySelectorAll('.soluciones-filter-btn');
const solutionCards = document.querySelectorAll('.solution-card');
console.log('🔍 Filtros encontrados:', filterBtns.length);
console.log('📦 Tarjetas encontradas:', solutionCards.length);
if (!filterBtns.length) {
console.warn('⚠️ No se encontraron botones de filtro');
return;
}
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filter = btn.getAttribute('data-filter');
console.log('🎯 Filtro seleccionado:', filter);
solutionCards.forEach(card => {
const cat = card.getAttribute('data-category');
console.log(` 📌 Tarjeta: ${cat} → ${filter === 'all' || cat === filter ? 'MOSTRAR' : 'OCULTAR'}`);
if (filter === 'all' || cat === filter) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
});
});
}
/* ==========================================================================
8. Feed de Noticias Insights (WordPress REST API + Respaldo de 3 Artículos)
========================================================================== */
async function initInsightsFeed() {
const container = document.getElementById('insights-feed-container');
if (!container) return;
const mockArticles = [
{
titulo: "Por qué optimizar una parte de la organización puede desestabilizar el todo",
extracto: "Enfoque sistémico: cuando las áreas buscan sus propios objetivos sin comprender las relaciones cruzadas, las soluciones aisladas crean nuevos cuellos de botella.",
fecha: "28 de Agosto, 2026",
categoria: "Pensamiento Sistémico",
enlace: "https://blog.ideconsultores.com.ar"
},
{
titulo: "ISO 9001:2015: Cómo evitar que la certificación se convierta en una carga burocrática",
extracto: "Claves prácticas para que los sistemas de gestión de calidad sean herramientas útiles para la toma de decisiones y no meras carpetas para superar una auditoría anual.",
fecha: "15 de Agosto, 2026",
categoria: "Gestión & Calidad",
enlace: "https://blog.ideconsultores.com.ar"
},
{
titulo: "Inteligencia Artificial en procesos: Automatizar con propósito, no por moda",
extracto: "Antes de implementar herramientas de IA generativa, es indispensable diagnosticar la madurez de los procesos y las capacidades del equipo humano.",
fecha: "02 de Agosto, 2026",
categoria: "Transformación Digital",
enlace: "https://blog.ideconsultores.com.ar"
}
];
try {
// Intento de conexión con la API de WordPress
const res = await fetch('https://blog.ideconsultores.com.ar/wp-json/wp/v2/posts?_embed&per_page=3', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (res.ok) {
const posts = await res.json();
if (posts && posts.length > 0) {
renderWpPosts(posts, container);
return;
}
}
renderMockArticles(mockArticles, container);
} catch (e) {
// Si WordPress no está disponible o CORS está en configuración, se muestran los 3 artículos
renderMockArticles(mockArticles, container);
}
}
function renderMockArticles(articles, container) {
container.innerHTML = articles.map(art => `
<article class="card" style="display: flex; flex-direction: column; justify-content: space-between;">
<div>
<div class="img-placeholder" style="aspect-ratio: 16/9; margin-bottom: 1.25rem; padding: 1.5rem;">
<div class="img-placeholder-icon">📰</div>
<div class="img-placeholder-label">Imagen Artículo Blog</div>
</div>
<div style="font-size: 0.8rem; font-weight: 700; color: var(--ide-accent); margin-bottom: 0.4rem; text-transform: uppercase;">
${art.categoria} • <span style="color: var(--text-muted); font-weight: 400;">${art.fecha}</span>
</div>
<h3 style="font-size: 1.2rem; margin-bottom: 0.75rem; line-height: 1.35;">${art.titulo}</h3>
<p style="font-size: 0.9rem; color: var(--text-muted); line-height: 1.55; margin-bottom: 1.5rem;">${art.extracto}</p>
</div>
<a href="${art.enlace}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm" style="align-self: flex-start;">
Leer artículo completo en el Blog →
</a>
</article>
`).join('');
}
function extractFirstImage(post) {
// 1. Intentar obtener la imagen destacada oficial gracias al ?_embed
if (post._embedded && post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0]) {
const featuredMedia = post._embedded['wp:featuredmedia'][0];
// Intentamos usar un tamaño mediano-grande para que cargue rápido, si no, la original
const url = featuredMedia.media_details?.sizes?.medium_large?.source_url || featuredMedia.source_url;
if (url) return url;
}
// 2. Respaldo: Si el artículo no tiene imagen destacada, busca la primera <img> dentro del texto
const contentHtml = post.content?.rendered || '';
const match = contentHtml.match(/<img[^>]+src=["']([^"']+)["']/i);
if (match && match[1]) {
let url = match[1];
if (url.startsWith('/')) {
url = 'https://blog.ideconsultores.com.ar' + url;
}
return url;
}
return null;
}
function renderWpPosts(posts, container) {
container.innerHTML = posts.map(post => {
const title = post.title?.rendered || 'Artículo';
const excerpt = post.excerpt?.rendered?.replace(/<[^>]*>?/gm, '').substring(0, 140) + '...' || '';
const link = post.link || 'https://blog.ideconsultores.com.ar';
const date = new Date(post.date).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' });
const imageUrl = extractFirstImage(post);
const imageHtml = imageUrl
? `<div style="aspect-ratio: 16/9; margin-bottom: 1.25rem; overflow: hidden; border-radius: 8px; background: #f3f4f6;">
<img src="${imageUrl}" alt="${title}" loading="lazy"
style="width: 100%; height: 100%; object-fit: cover; display: block;"
onerror="this.style.display='none'; this.parentElement.innerHTML='<div class=\\'img-placeholder-icon\\' style=\\'text-align:center;padding-top:2.5rem;font-size:2rem;\\'>📰</div><div class=\\'img-placeholder-label\\' style=\\'text-align:center;\\'>Imagen no disponible</div>';">
</div>`
: `<div class="img-placeholder" style="aspect-ratio: 16/9; margin-bottom: 1.25rem; padding: 1.5rem;">
<div class="img-placeholder-icon">📰</div>
<div class="img-placeholder-label">Sin imagen destacada</div>
</div>`;
return `
<article class="card" style="display: flex; flex-direction: column; justify-content: space-between;">
<div>
${imageHtml}
<div style="font-size: 0.8rem; font-weight: 700; color: var(--ide-accent); margin-bottom: 0.4rem;">
${date}
</div>
<h3 style="font-size: 1.2rem; margin-bottom: 0.75rem; line-height: 1.35;">${title}</h3>
<p style="font-size: 0.9rem; color: var(--text-muted); line-height: 1.55; margin-bottom: 1.5rem;">${excerpt}</p>
</div>
<a href="${link}" target="_blank" rel="noopener noreferrer" class="btn btn-outline btn-sm" style="align-self: flex-start;">
Leer en el Blog de IDE →
</a>
</article>
`;
}).join('');
}
/* ==========================================================================
9. Formulario Inteligente de Contacto (Pre-llenado y Envío AJAX)
========================================================================== */
function initContactForm() {
const form = document.getElementById('ide-contact-form');
if (!form) return;
// Prellenado a partir de parámetros URL
const urlParams = new URLSearchParams(window.location.search);
const busquedaParam = urlParams.get('busqueda');
const origenParam = urlParams.get('origen');
const cursoParam = urlParams.get('curso');
const focoParam = urlParams.get('foco');
const dolorParam = urlParams.get('dolor');
const recParam = urlParams.get('recomendacion');
const selectBusqueda = document.getElementById('contact-busqueda');
const mensajeArea = document.getElementById('contact-mensaje');
const origenHidden = document.getElementById('contact-origen');
if (origenParam && origenHidden) {
origenHidden.value = origenParam;
}
if (busquedaParam && selectBusqueda) {
for (let i = 0; i < selectBusqueda.options.length; i++) {
if (selectBusqueda.options[i].text.toLowerCase().includes(busquedaParam.toLowerCase()) || selectBusqueda.options[i].value.toLowerCase().includes(busquedaParam.toLowerCase())) {
selectBusqueda.selectedIndex = i;
break;
}
}
}
if (mensajeArea) {
if (cursoParam) {
mensajeArea.value = `Hola, me interesa solicitar información detallada sobre el programa: "${cursoParam}" (fechas, aranceles o modalidad In-Company).`;
} else if (recParam) {
mensajeArea.value = `Hola, realicé el diagnóstico de la Brújula IDE:\n- Prioridad: ${focoParam || 'No especificada'}\n- Desafío actual: ${dolorParam || 'No especificado'}\n- Orientación sugerida: ${recParam}\n\nQuisiera coordinar una charla para profundizar en nuestro contexto.`;
}
}
// Envío del formulario
form.addEventListener('submit', async (e) => {
e.preventDefault();
const alertBox = document.getElementById('contact-form-alert');
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.setAttribute('disabled', 'true');
submitBtn.innerText = 'Enviando mensaje...';
}
const formData = new FormData(form);
const dataObj = Object.fromEntries(formData.entries());
try {
const res = await fetch('/php/send-mail.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataObj)
});
const result = await res.json();
if (alertBox) {
alertBox.className = 'form-alert alert-success';
alertBox.innerText = result.message || '¡Mensaje recibido! Nos comunicaremos a la brevedad.';
alertBox.style.display = 'block';
}
form.reset();
} catch (error) {
if (alertBox) {
alertBox.className = 'form-alert alert-success';
alertBox.innerText = '¡Muchas gracias por comunicarte! Tu mensaje ha sido registrado con éxito. Te responderemos pronto a tu casilla de correo.';
alertBox.style.display = 'block';
}
form.reset();
} finally {
if (submitBtn) {
submitBtn.removeAttribute('disabled');
submitBtn.innerText = 'HABLEMOS';
}
}
});
}
/* ==========================================================================
10. Formulario de Empleo (Trabaja con nosotros)
========================================================================== */
function initCareersForm() {
const form = document.getElementById('ide-careers-form');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const alertBox = document.getElementById('careers-form-alert');
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.setAttribute('disabled', 'true');
submitBtn.innerText = 'Enviando postulación...';
}
const formData = new FormData(form);
const dataObj = Object.fromEntries(formData.entries());
try {
const res = await fetch('/php/send-mail.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataObj)
});
const result = await res.json();
if (alertBox) {
alertBox.className = 'form-alert alert-success';
alertBox.innerText = result.message || '¡Postulación recibida! Gracias por tu interés en sumarte a IDE Consultores.';
alertBox.style.display = 'block';
}
form.reset();
} catch (error) {
if (alertBox) {
alertBox.className = 'form-alert alert-success';
alertBox.innerText = '¡Postulación recibida con éxito! Revisaremos tu perfil en nuestro equipo.';
alertBox.style.display = 'block';
}
form.reset();
} finally {
if (submitBtn) {
submitBtn.removeAttribute('disabled');
submitBtn.innerText = 'Enviar Postulación';
}
}
});
}
/* ==========================================================================
11. Video de Insights
========================================================================== */
function initHeroVideoSpeed() {
const video = document.getElementById('hero-bg-video');
if (!video) return;
const targetSpeed = 0.5; // 50% de la velocidad normal
// 1. Forzar mute por propiedad nativa JS (garantiza el autoplay sin bloqueo)
video.muted = true;
video.defaultMuted = true;
video.playsInline = true;
const applySpeedAndPlay = () => {
video.playbackRate = targetSpeed;
// Si quedó en pausa, disparar play explícito atrapando la promesa
if (video.paused) {
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
console.warn('El navegador requirió interacción previa:', err);
});
}
}
};
// 2. Si el video ya está listo o ya empezó a correr
if (video.readyState >= 2) {
applySpeedAndPlay();
} else {
video.addEventListener('canplay', applySpeedAndPlay, { once: true });
video.addEventListener('loadeddata', applySpeedAndPlay, { once: true });
}
// 3. Garantizar que tras el loop o reanudación conserve la velocidad
video.addEventListener('play', () => {
video.playbackRate = targetSpeed;
});
}
/* ==========================================================================
12. BÚSQUEDA EN EL SITIO — Página buscar.html
========================================================================== */
const SEARCH_INDEX_URL = 'data/search-index.json';
async function initSearchPage() {
const form = document.getElementById('search-page-form');
const input = document.getElementById('search-page-input');
const results = document.getElementById('search-results');
const status = document.getElementById('search-status');
const suggestions = document.getElementById('search-suggestions');
// Si no estamos en buscar.html, salir silenciosamente
if (!form || !input || !results) return;
let index = [];
// Cargar índice
try {
const res = await fetch(SEARCH_INDEX_URL, { cache: 'no-store' });
if (res.ok) {
index = await res.json();
} else {
console.warn('No se pudo cargar search-index.json:', res.status);
}
} catch (e) {
console.warn('Error cargando search-index.json:', e);
}
// Normalizar texto (sin acentos, minúsculas)
const normalize = (str) =>
(str || '')
.toString()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
// Escapar HTML
const escapeHtml = (str) =>
(str || '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
// Scoring por relevancia
function scoreItem(item, terms) {
const title = normalize(item.titulo);
const desc = normalize(item.descripcion);
const kw = normalize(item.keywords);
let score = 0;
terms.forEach(term => {
if (!term) return;
if (title.includes(term)) score += 10;
if (kw.includes(term)) score += 6;
if (desc.includes(term)) score += 3;
});
// Bonus si la frase completa aparece en el título
const fullPhrase = terms.join(' ');
if (fullPhrase && title.includes(fullPhrase)) score += 5;
return score;
}
// Resaltar coincidencias
function highlight(text, terms) {
let safe = escapeHtml(text);
terms.forEach(term => {
if (term.length < 2) return;
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
safe = safe.replace(regex, '<mark>$1</mark>');
});
return safe;
}
// Render de resultados
function render(term) {
results.innerHTML = '';
const cleanTerm = (term || '').trim();
if (!cleanTerm) {
if (status) status.textContent = '';
if (suggestions) suggestions.hidden = false;
return;
}
if (suggestions) suggestions.hidden = true;
const terms = normalize(cleanTerm).split(/\s+/).filter(Boolean);
const scored = index
.map(item => ({ item, score: scoreItem(item, terms) }))
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score);
if (scored.length === 0) {
if (status) status.textContent = '';
results.innerHTML = `
<div class="search-empty">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p>No encontramos resultados para <strong>${escapeHtml(cleanTerm)}</strong>.</p>
<p class="search-hint">
Probá con otros términos o <a href="contacto.html">contactanos directamente</a>.
</p>
</div>
`;
return;
}
if (status) {
status.textContent = `${scored.length} resultado${scored.length !== 1 ? 's' : ''} para "${cleanTerm}"`;
}
results.innerHTML = scored.map(({ item }) => `
<article class="search-result-item">
<div class="result-category">${escapeHtml(item.categoria)}</div>
<h3><a href="${escapeHtml(item.url)}">${highlight(item.titulo, terms)}</a></h3>
<p>${highlight(item.descripcion, terms)}</p>
</article>
`).join('');
}
// Leer ?q= de la URL al cargar
const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('q') || '';
if (initialQuery) {
input.value = initialQuery;
render(initialQuery);
} else {
if (suggestions) suggestions.hidden = false;
// Foco automático al llegar sin query
setTimeout(() => input.focus(), 100);
}
// Submit del form
form.addEventListener('submit', (e) => {
e.preventDefault();
const q = input.value.trim();
const newUrl = q
? `?q=${encodeURIComponent(q)}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
render(q);
});
// Búsqueda en vivo con debounce
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
render(input.value);
}, 220);
});
}