base de cursos desde google sheets

This commit is contained in:
2026-09-27 17:14:52 -03:00
parent 3cf66793ae
commit c5c589b32e
2 changed files with 193 additions and 57 deletions
+3 -1
View File
@@ -146,6 +146,8 @@
<h2>Elegí qué capacidad querés desarrollar.</h2>
<p class="lead-text">
Cursos y programas diseñados para conectar conocimiento, práctica y contexto, con herramientas que podés llevar a tu realidad profesional desde el primer día.
</p>
</div>
</div>
<!-- Contenedor inyectado por JS desde /data/cursos.json -->
@@ -326,7 +328,7 @@
<div id="modal-course-view" class="modal-backdrop" role="dialog" aria-modal="true">
<div class="modal-window">
<button class="modal-close-btn" aria-label="Cerrar modal">✕</button>
<span id="modal-course-category" class="eyebrow" style="color: #D97706;">PROGRAMA EJECUTIVO</span>
<div id="modal-course-category" class="eyebrow">PROGRAMA EJECUTIVO</div>
<h3 id="modal-course-title" style="margin-bottom: 1.25rem; color: var(--ide-primary);">Título del Curso</h3>
<div id="modal-course-details">
+190 -56
View File
@@ -496,6 +496,8 @@ function closeModal(modalId) {
/* ==========================================================================
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() {
@@ -503,43 +505,136 @@ async function initAcademyCourses() {
if (!container) return;
try {
const res = await fetch('/data/cursos.json');
if (!res.ok) throw new Error('No se pudo cargar cursos.json');
allCourses = await res.json();
// 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('Cargando cursos con datos de respaldo.');
allCourses = getFallbackCourses();
renderCourseCards(allCourses, container);
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.innerHTML = `
<div class="ecosystem-badge badge-academy" style="margin-bottom: 0.8rem;">${course.categoria}</div>
<h3 style="font-size: 1.15rem; margin-bottom: 0.6rem;">${course.titulo}</h3>
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 1.25rem; line-height: 1.5;">${course.descripcion_corta}</p>
<div style="font-size: 0.82rem; color: var(--text-body); display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 1.5rem; border-top: 1px solid var(--border-color); padding-top: 0.85rem;">
<div><strong>Modalidad:</strong> ${course.modalidad}</div>
<div><strong>Duración:</strong> ${course.duracion}</div>
<div><strong>Próxima fecha:</strong> ${course.proxima_fecha}</div>
</div>
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.justifyContent = 'space-between';
<button class="btn btn-outline btn-sm w-full" style="width: 100%;" onclick="openCourseModal('${course.id}')">
Ver Ficha Técnica y Programa
</button>
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);
});
}
// Modal dinámico de cursos
// 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;
@@ -551,10 +646,20 @@ window.openCourseModal = function (courseId) {
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">
<span class="course-badge-category">${course.categoria}</span>
<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">
@@ -569,7 +674,7 @@ window.openCourseModal = function (courseId) {
</div>
<div class="spec-text">
<span class="spec-label">Modalidad</span>
<strong class="spec-value">${course.modalidad}</strong>
<strong class="spec-value">${modalidad}</strong>
</div>
</div>
@@ -583,25 +688,26 @@ window.openCourseModal = function (courseId) {
</div>
<div class="spec-text">
<span class="spec-label">Duración</span>
<strong class="spec-value">${course.duracion}</strong>
<strong class="spec-value">${duracion}</strong>
</div>
</div>
<!-- Sector -->
<!-- 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">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
<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">Sector</span>
<strong class="spec-value">${course.sector || '—'}</strong>
<span class="spec-label">Nivel</span>
<strong class="spec-value">${nivel}</strong>
</div>
</div>
<!-- Próxima Fecha -->
<!-- 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">
@@ -612,8 +718,10 @@ window.openCourseModal = function (courseId) {
</svg>
</div>
<div class="spec-text">
<span class="spec-label">Próxima fecha</span>
<strong class="spec-value highlight-date">${course.proxima_fecha || 'A consultar'}</strong>
<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>
@@ -621,32 +729,46 @@ window.openCourseModal = function (courseId) {
}
if (detailsElem) {
const objHtml = course.objetivos.map(obj => `<li>${obj}</li>`).join('');
const temHtml = course.temario.map(tem => `<li>${tem}</li>`).join('');
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);">${course.propuesta}</p>
<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;">Objetivos del Programa</h4>
<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;">
${objHtml}
${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;">Temario Modular</h4>
<ul style="padding-left: 1.25rem; font-size: 0.9rem; color: var(--text-body); line-height: 1.6;">
${temHtml}
<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>
<div style="background-color: var(--bg-subtle); padding: 1rem; border-radius: var(--radius-sm); font-size: 0.88rem; margin-bottom: 1.5rem;">
<p><strong>Cuerpo Docente:</strong> ${course.docente}</p>
<p><strong>Impacto Esperado:</strong> ${course.resultados_esperados}</p>
<!-- 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>
`;
}
@@ -655,7 +777,7 @@ window.openCourseModal = function (courseId) {
const params = new URLSearchParams({
origen: 'IDE_Academy',
busqueda: 'Formación para mi organización',
curso: course.titulo
curso: `${course.titulo} (${course.id})`
});
ctaElem.setAttribute('href', `contacto.html?${params.toString()}`);
}
@@ -666,18 +788,30 @@ window.openCourseModal = function (courseId) {
function getFallbackCourses() {
return [
{
id: "curso-iso-9001",
titulo: "Sistemas de Gestión de Calidad (ISO 9001:2015): De la Norma a la Gestión Real",
categoria: "IDE Excellence & Academy",
modalidad: "Online sincrónico / In-Company",
duracion: "32 horas (8 semanas)",
descripcion_corta: "Aprenda a diseñar, implementar y auditar un SGC que trascienda la mera auditoría y transforme los procesos.",
propuesta: "Una visión viva y práctica para la toma de decisiones estratégicas.",
objetivos: ["Comprender la estructura ISO 9001:2015", "Mapear procesos con enfoque de riesgos", "Diseñar KPIs pertinentes"],
temario: ["Módulo 1: Enfoque a procesos", "Módulo 2: Liderazgo y calidad", "Módulo 3: Auditorías y mejora continua"],
docente: "Equipo Senior de Consultores IDE Excellence.",
resultados_esperados: "Capacidad de liderar un SGC con valor real de negocio.",
proxima_fecha: "Octubre 2026"
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."
}
];
}