base de cursos desde google sheets
This commit is contained in:
+3
-1
@@ -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
@@ -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."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user