<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
:root {
--gold: #C9A84C;
--gold-light: #E8C97A;
--gold-pale: #FBF4E3;
--dark: #1A1A2E;
--dark2: #16213E;
--ink: #2C2C3E;
--muted: #7A7A9A;
--border: #E2D9C5;
--white: #FDFCF9;
--green: #2D6A4F;
--red: #C0392B;
--blue: #1B4F8A;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
/* HEADER */
.header {
background: linear-gradient(135deg, var(--dark) 0%, var(--dark2) 60%, #0F3460 100%);
padding: 40px 48px 32px;
border-bottom: 2px solid var(--gold);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -60px; right: -60px;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(201,168,76,0.15) 0%, transparent 70%);
}
.header-badge {
display: inline-block;
background: var(--gold);
color: var(--dark);
font-size: 11px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
padding: 5px 14px;
border-radius: 2px;
margin-bottom: 14px;
}
.header h1 {
font-family: 'Playfair Display', serif;
color: var(--white);
font-size: 30px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 6px;
}
.header h1 span { color: var(--gold-light); }
.header p {
color: rgba(255,255,255,0.55);
font-size: 13px;
letter-spacing: 0.5px;
}
/* MAIN LAYOUT */
.container {
max-width: 960px;
margin: 0 auto;
padding: 36px 24px 80px;
}
/* STEPS */
.steps {
display: flex;
gap: 0;
margin-bottom: 36px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(201,168,76,0.25);
border-radius: 8px;
overflow: hidden;
}
.step {
flex: 1;
padding: 14px 18px;
text-align: center;
cursor: pointer;
border-right: 1px solid rgba(201,168,76,0.15);
transition: all 0.2s;
position: relative;
}
.step:last-child { border-right: none; }
.step .step-num {
font-size: 10px;
color: var(--muted);
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 3px;
}
.step .step-label {
font-size: 12px;
color: rgba(255,255,255,0.5);
font-weight: 500;
}
.step.active { background: rgba(201,168,76,0.12); }
.step.active .step-num { color: var(--gold); }
.step.active .step-label { color: var(--white); }
.step.done .step-label { color: rgba(255,255,255,0.35); }
.step.done::after {
content: '✓';
position: absolute;
top: 8px; right: 10px;
color: var(--gold);
font-size: 10px;
}
/* PANELS */
.panel { display: none; animation: fadeIn 0.3s ease; }
.panel.visible { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
/* CARD */
.card {
background: var(--white);
border-radius: 10px;
padding: 32px 36px;
margin-bottom: 24px;
box-shadow: 0 4px 24px rgba(0,0,0,0.18);
}
.card-title {
font-family: 'Playfair Display', serif;
font-size: 18px;
color: var(--dark);
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 10px;
}
.card-title .icon {
width: 28px; height: 28px;
background: var(--gold);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 13px;
flex-shrink: 0;
}
.card-sub {
font-size: 12px;
color: var(--muted);
margin-bottom: 24px;
padding-left: 38px;
}
/* FORM ELEMENTS */
label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--ink);
letter-spacing: 0.5px;
text-transform: uppercase;
margin-bottom: 6px;
}
input[type="text"], input[type="email"], select, textarea {
width: 100%;
padding: 11px 14px;
border: 1.5px solid var(--border);
border-radius: 6px;
font-family: 'DM Sans', sans-serif;
font-size: 14px;
color: var(--ink);
background: var(--white);
transition: border-color 0.2s;
outline: none;
margin-bottom: 18px;
}
input:focus, select:focus, textarea:focus {
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(201,168,76,0.12);
}
textarea { resize: vertical; min-height: 80px; }
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* CATEGORY SELECTOR */
.cat-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 8px;
}
.cat-card {
border: 2px solid var(--border);
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.cat-card:hover { border-color: var(--gold); background: var(--gold-pale); }
.cat-card.selected { border-color: var(--gold); background: var(--gold-pale); }
.cat-card .cat-icon { font-size: 28px; margin-bottom: 10px; }
.cat-card .cat-name {
font-family: 'Playfair Display', serif;
font-size: 14px;
font-weight: 600;
color: var(--dark);
margin-bottom: 6px;
}
.cat-card .cat-desc { font-size: 11px; color: var(--muted); line-height: 1.4; }
/* SUBCATEGORY */
.subcat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 8px;
}
.subcat-item {
border: 1.5px solid var(--border);
border-radius: 6px;
padding: 12px 14px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.subcat-item:hover { border-color: var(--gold); background: var(--gold-pale); }
.subcat-item.selected { border-color: var(--gold); background: var(--gold-pale); }
.subcat-item .num {
width: 22px; height: 22px;
background: var(--gold);
border-radius: 50%;
font-size: 10px;
font-weight: 700;
color: var(--dark);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.subcat-item.selected .num { background: var(--dark); color: var(--gold); }
.subcat-label { font-size: 12px; font-weight: 500; color: var(--ink); line-height: 1.3; }
/* RUBRIC TABLE */
.rubric-section { margin-bottom: 28px; }
.rubric-section-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.criterion-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
align-items: start;
padding: 16px 0;
border-bottom: 1px solid #f0ede6;
}
.criterion-row:last-child { border-bottom: none; }
.criterion-info .crit-name {
font-size: 14px;
font-weight: 600;
color: var(--dark);
margin-bottom: 3px;
}
.criterion-info .crit-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
.criterion-info .crit-weight {
display: inline-block;
background: var(--gold-pale);
border: 1px solid var(--gold);
color: var(--dark);
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 20px;
margin-top: 6px;
letter-spacing: 0.5px;
}
.score-buttons {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.score-btn {
width: 38px; height: 38px;
border: 1.5px solid var(--border);
border-radius: 6px;
background: white;
font-size: 14px;
font-weight: 700;
color: var(--muted);
cursor: pointer;
transition: all 0.15s;
display: flex; align-items: center; justify-content: center;
}
.score-btn:hover { border-color: var(--gold); color: var(--dark); }
.score-btn.active { background: var(--gold); border-color: var(--gold); color: var(--dark); }
/* SCORE LEGEND */
.score-legend {
background: #f8f6f0;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 24px;
}
.score-legend-title { font-size: 11px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.legend-items { display: flex; gap: 10px; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--ink); }
.legend-dot {
width: 22px; height: 22px;
background: var(--gold);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: var(--dark);
}
/* RESULT BOX */
.result-box {
background: linear-gradient(135deg, var(--dark) 0%, var(--dark2) 100%);
border: 1px solid var(--gold);
border-radius: 10px;
padding: 24px 28px;
margin-top: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.result-box .score-display {
font-family: 'Playfair Display', serif;
font-size: 48px;
font-weight: 700;
color: var(--gold-light);
}
.result-box .score-label { font-size: 12px; color: rgba(255,255,255,0.5); margin-top: 2px; }
.result-box .score-bar-wrap { flex: 1; margin: 0 32px; }
.score-bar-bg { height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }
.score-bar-fill { height: 100%; background: linear-gradient(90deg, var(--gold), var(--gold-light)); border-radius: 4px; transition: width 0.6s cubic-bezier(0.4,0,0.2,1); }
.score-level { font-size: 13px; color: var(--gold-light); font-weight: 600; margin-top: 8px; }
/* BUTTONS */
.btn {
padding: 13px 28px;
border-radius: 7px;
font-family: 'DM Sans', sans-serif;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s;
letter-spacing: 0.3px;
}
.btn-primary {
background: var(--gold);
color: var(--dark);
}
.btn-primary:hover { background: var(--gold-light); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(201,168,76,0.4); }
.btn-secondary {
background: transparent;
color: rgba(255,255,255,0.7);
border: 1.5px solid rgba(255,255,255,0.2);
}
.btn-secondary:hover { border-color: var(--gold); color: var(--gold); }
.btn-export {
background: var(--green);
color: white;
}
.btn-export:hover { background: #1e4d38; transform: translateY(-1px); }
.btn-add {
background: var(--blue);
color: white;
}
.btn-add:hover { background: #123666; }
.btn-row {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 28px;
}
/* EVALUATIONS LIST */
.eval-list { margin-bottom: 20px; }
.eval-item {
background: var(--white);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.eval-item-info { display: flex; flex-direction: column; gap: 3px; }
.eval-item-title { font-size: 14px; font-weight: 600; color: var(--dark); }
.eval-item-meta { font-size: 11px; color: var(--muted); }
.eval-score-badge {
background: var(--gold);
color: var(--dark);
font-weight: 700;
font-size: 14px;
padding: 6px 14px;
border-radius: 20px;
min-width: 60px;
text-align: center;
}
.eval-delete {
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 16px;
padding: 4px 8px;
transition: color 0.2s;
}
.eval-delete:hover { color: var(--red); }
/* SUMMARY TABLE */
.summary-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
font-size: 13px;
}
.summary-table th {
background: var(--dark);
color: var(--gold);
padding: 10px 14px;
text-align: left;
font-size: 11px;
letter-spacing: 0.8px;
text-transform: uppercase;
}
.summary-table td {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
color: var(--ink);
}
.summary-table tr:hover td { background: var(--gold-pale); }
.tag {
display: inline-block;
font-size: 10px;
padding: 2px 8px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
}
.tag-transform { background: #E8F4FD; color: var(--blue); }
.tag-vida { background: #FEF9E7; color: #856404; border: 1px solid var(--gold); }
/* CONFLICT SECTION */
.conflict-box {
border: 1.5px dashed var(--border);
border-radius: 8px;
padding: 16px 20px;
margin-top: 20px;
}
.conflict-box label { text-transform: none; font-size: 13px; font-weight: 500; letter-spacing: 0; display: flex; align-items: center; gap: 8px; cursor: pointer; }
.conflict-box input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--gold); }
/* EMPTY STATE */
.empty-state {
text-align: center;
padding: 48px 20px;
color: rgba(255,255,255,0.4);
}
.empty-state .empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
.empty-state p { font-size: 14px; }
/* TOAST */
.toast {
position: absolute;
bottom: 24px;
right: 24px;
background: var(--dark);
border: 1px solid var(--gold);
color: var(--white);
padding: 14px 20px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
transform: translateY(80px);
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
max-width: 300px;
}
.toast.show { transform: translateY(0); opacity: 1; }
/* RESPONSIVE */
@media (max-width: 620px) {
.form-row, .cat-cards, .subcat-grid { grid-template-columns: 1fr; }
.score-buttons { flex-wrap: wrap; }
.card { padding: 20px; }
.container { padding: 20px 14px 60px; }
.header { padding: 28px 20px 22px; }
}
</style>
<div class="header">
<div class="header-badge">II Jornada · 2026</div>
<h1>Reconocimientos a <span>Bibliotecas Transformadoras</span></h1>
<p>Sistema de Evaluación para Jurados · 10 al 31 de marzo de 2026</p>
</div>
<div class="container">
<!-- STEPS -->
<div class="steps">
<div class="step active" id="step1" onclick="goToStep(1)">
<div class="step-num">Paso 1</div>
<div class="step-label">Identificación</div>
</div>
<div class="step" id="step2" onclick="goToStep(2)">
<div class="step-num">Paso 2</div>
<div class="step-label">Categoría</div>
</div>
<div class="step" id="step3" onclick="goToStep(3)">
<div class="step-num">Paso 3</div>
<div class="step-label">Calificación</div>
</div>
<div class="step" id="step4" onclick="goToStep(4)">
<div class="step-num">Paso 4</div>
<div class="step-label">Resumen</div>
</div>
</div>
<!-- PANEL 1: IDENTIFICACIÓN -->
<div class="panel visible" id="panel1">
<div class="card">
<div class="card-title"><div class="icon">👤</div> Identificación del Jurado</div>
<div class="card-sub">Complete sus datos antes de iniciar las evaluaciones.</div>
<div class="form-row">
<div>
<label>Nombre completo *</label>
<input type="text" id="juradoNombre" placeholder="Ej. Dra. María González">
</div>
<div>
<label>Código / Identificador</label>
<input type="text" id="juradoCodigo" placeholder="Ej. JUR-001">
</div>
</div>
<div class="form-row">
<div>
<label>Institución</label>
<input type="text" id="juradoInstitucion" placeholder="Ej. Universidad Nacional">
</div>
<div>
<label>Correo electrónico</label>
<input type="email" id="juradoEmail" placeholder="correo@institucion.edu.co">
</div>
</div>
<div class="conflict-box">
<label>
<input type="checkbox" id="conflictoInteres">
Declaro que conozco y no tengo conflicto de interés con los proyectos/postulantes que evaluaré.
</label>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="continueToStep2()">Continuar →</button>
</div>
</div>
</div>
<!-- PANEL 2: CATEGORÍA -->
<div class="panel" id="panel2">
<div class="card">
<div class="card-title"><div class="icon">📂</div> Seleccionar Categoría</div>
<div class="card-sub">Elija la categoría del proyecto o postulación a evaluar.</div>
<div class="cat-cards">
<div class="cat-card" id="cat-transform" onclick="selectCategory('transformadores')">
<div class="cat-icon">🏛️</div>
<div class="cat-name">Proyectos Transformadores</div>
<div class="cat-desc">10 subcategorías · Evaluación de proyectos institucionales</div>
</div>
<div class="cat-card" id="cat-vida" onclick="selectCategory('vida')">
<div class="cat-icon">🌟</div>
<div class="cat-name">Reconocimiento a Toda una Vida</div>
<div class="cat-desc">Trayectoria profesional sostenida en el sector bibliotecario</div>
</div>
</div>
<!-- SUBCATEGORIES (for transformadores) -->
<div id="subcatSection" style="display:none; margin-top:24px;">
<label style="text-transform:none; font-size:13px; font-weight:600; letter-spacing:0; margin-bottom:12px; display:block;">Seleccione la subcategoría:</label>
<div class="subcat-grid" id="subcatGrid"></div>
</div>
<!-- POSTULANT INFO -->
<div style="margin-top:24px;">
<label>Nombre del proyecto / postulante *</label>
<input type="text" id="postulanteName" placeholder="Ej. Biblioteca Central UNAL / Nombre del profesional">
<label>Institución postulante</label>
<input type="text" id="postulanteInstitucion" placeholder="Ej. Universidad de Antioquia">
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="goToStep(1)">← Atrás</button>
<button class="btn btn-primary" onclick="continueToStep3()">Calificar →</button>
</div>
</div>
</div>
<!-- PANEL 3: CALIFICACIÓN -->
<div class="panel" id="panel3">
<div class="card">
<div class="card-title"><div class="icon">⭐</div> Rúbrica de Evaluación</div>
<div class="card-sub" id="panel3Sub"></div>
<div class="score-legend">
<div class="score-legend-title">Escala de calificación</div>
<div class="legend-items">
<div class="legend-item"><div class="legend-dot">1</div> Muy bajo / Sin evidencia</div>
<div class="legend-item"><div class="legend-dot">2</div> Bajo / Limitado</div>
<div class="legend-item"><div class="legend-dot">3</div> Aceptable / Parcial</div>
<div class="legend-item"><div class="legend-dot">4</div> Alto / Cumple</div>
<div class="legend-item"><div class="legend-dot">5</div> Sobresaliente</div>
</div>
</div>
<div id="rubricContainer"></div>
<!-- QUALITATIVE (Vida) -->
<div id="qualitativeSection" style="display:none;">
<div class="rubric-section-title" style="margin-top:24px;">Evaluación Cualitativa</div>
<label>¿Cuál considera que es la principal contribución de este(a) profesional?</label>
<textarea id="q1" placeholder="Escriba su respuesta..."></textarea>
<label>¿Por qué su trayectoria merece reconocimiento público?</label>
<textarea id="q2" placeholder="Escriba su respuesta..."></textarea>
</div>
<!-- OBSERVATIONS -->
<div style="margin-top:16px;">
<label style="text-transform:none; font-size:13px; font-weight:500; letter-spacing:0;">Observaciones adicionales (opcional)</label>
<textarea id="observaciones" placeholder="Comentarios generales del jurado..."></textarea>
</div>
<!-- RESULT -->
<div class="result-box">
<div>
<div class="score-display" id="scoreDisplay">—</div>
<div class="score-label">Puntaje total</div>
</div>
<div class="score-bar-wrap">
<div class="score-bar-bg">
<div class="score-bar-fill" id="scoreBar" style="width:0%"></div>
</div>
<div class="score-level" id="scoreLevel">Complete todos los criterios</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="goToStep(2)">← Atrás</button>
<button class="btn btn-primary" onclick="saveEvaluation()">💾 Guardar Evaluación</button>
</div>
</div>
</div>
<!-- PANEL 4: RESUMEN -->
<div class="panel" id="panel4">
<div class="card">
<div class="card-title"><div class="icon">📊</div> Resumen de Evaluaciones</div>
<div class="card-sub" id="panel4Sub"></div>
<div id="evalListContainer"></div>
<div id="emptyState" class="empty-state" style="display:none;">
<div class="empty-icon">📋</div>
<p>No hay evaluaciones registradas aún.</p>
</div>
<div id="summaryTableWrap" style="display:none; overflow-x:auto;">
<table class="summary-table">
<thead>
<tr>
<th>#</th>
<th>Proyecto / Postulante</th>
<th>Categoría</th>
<th>Institución</th>
<th>Puntaje</th>
<th>Nivel</th>
</tr>
</thead>
<tbody id="summaryBody"></tbody>
</table>
</div>
<div class="btn-row" style="margin-top:24px; justify-content:space-between; flex-wrap:wrap; gap:10px;">
<button class="btn btn-add" onclick="newEvaluation()">+ Nueva Evaluación</button>
<div style="display:flex; gap:12px;">
<button class="btn btn-export" onclick="exportToExcel()">⬇ Exportar Excel</button>
</div>
</div>
</div>
</div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"></div>
<script>
// ============================================================
// DATA
// ============================================================
const SUBCATEGORIES = [
{ id: 1, name: "Innovación Tecnológica" },
{ id: 2, name: "Fomento a la Lectura Lúdica" },
{ id: 3, name: "Colaboración Interinstitucional" },
{ id: 4, name: "Impacto Social" },
{ id: 5, name: "Impacto Ambiental" },
{ id: 6, name: "Inclusión y Accesibilidad" },
{ id: 7, name: "Alfabetización Informacional" },
{ id: 8, name: "Diseño de Espacios Físicos" },
{ id: 9, name: "Uso Creativo de Recursos Bibliográficos" },
{ id: 10, name: "Red de Colaboración de Bibliotecas Universitarias" }
];
const CRITERIA_TRANSFORM = [
{ id: "c1", name: "Claridad del problema identificado", weight: 0.15, desc: "¿El proyecto identifica claramente el problema o necesidad que busca resolver?" },
{ id: "c2", name: "Innovación y diferenciación", weight: 0.20, desc: "¿Propone soluciones novedosas o enfoques diferenciadores respecto a prácticas tradicionales?" },
{ id: "c3", name: "Impacto medible", weight: 0.25, desc: "¿Se presentan evidencias cuantitativas o cualitativas del impacto generado?" },
{ id: "c4", name: "Sostenibilidad y escalabilidad", weight: 0.20, desc: "¿El proyecto puede mantenerse en el tiempo y replicarse en otros contextos?" },
{ id: "c5", name: "Gestión y ejecución del proyecto", weight: 0.20, desc: "¿Muestra evidencias de planificación, ejecución eficiente y logro de resultados?" },
];
const CRITERIA_VIDA = [
{ id: "v1", name: "Trayectoria y permanencia en el sector", weight: 0.20, desc: "Años de dedicación, continuidad y liderazgo sostenido en el ámbito bibliotecario o académico." },
{ id: "v2", name: "Impacto institucional y comunitario", weight: 0.25, desc: "Transformaciones generadas en bibliotecas o instituciones; influencia directa en comunidades académicas." },
{ id: "v3", name: "Aporte al desarrollo del sector", weight: 0.25, desc: "Participación en redes, producción académica, incidencia en políticas o modelos de gestión." },
{ id: "v4", name: "Innovación y legado profesional", weight: 0.30, desc: "Prácticas pioneras, modelos replicables, creación de metodologías o enfoques diferenciadores." },
];
// ============================================================
// STATE
// ============================================================
let state = {
currentStep: 1,
jurado: {},
currentCategory: null,
currentSubcat: null,
scores: {},
evaluations: [],
};
// ============================================================
// NAVIGATION
// ============================================================
function goToStep(n) {
for (let i = 1; i <= 4; i++) {
document.getElementById('panel' + i).classList.remove('visible');
const s = document.getElementById('step' + i);
s.classList.remove('active', 'done');
if (i < n) s.classList.add('done');
}
document.getElementById('panel' + n).classList.add('visible');
document.getElementById('step' + n).classList.add('active');
state.currentStep = n;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function continueToStep2() {
const nombre = document.getElementById('juradoNombre').value.trim();
if (!nombre) { showToast('⚠️ Ingrese su nombre completo.'); return; }
if (!document.getElementById('conflictoInteres').checked) { showToast('⚠️ Debe declarar el conflicto de interés.'); return; }
state.jurado = {
nombre,
codigo: document.getElementById('juradoCodigo').value.trim(),
institucion: document.getElementById('juradoInstitucion').value.trim(),
email: document.getElementById('juradoEmail').value.trim(),
};
updatePanel4Header();
goToStep(2);
}
function continueToStep3() {
if (!state.currentCategory) { showToast('⚠️ Seleccione una categoría.'); return; }
if (state.currentCategory === 'transformadores' && !state.currentSubcat) { showToast('⚠️ Seleccione la subcategoría.'); return; }
const name = document.getElementById('postulanteName').value.trim();
if (!name) { showToast('⚠️ Ingrese el nombre del proyecto o postulante.'); return; }
state.scores = {};
buildRubric();
goToStep(3);
}
// ============================================================
// CATEGORY SELECTION
// ============================================================
function selectCategory(cat) {
state.currentCategory = cat;
state.currentSubcat = null;
document.getElementById('cat-transform').classList.toggle('selected', cat === 'transformadores');
document.getElementById('cat-vida').classList.toggle('selected', cat === 'vida');
const subcatSection = document.getElementById('subcatSection');
if (cat === 'transformadores') {
subcatSection.style.display = 'block';
buildSubcatGrid();
} else {
subcatSection.style.display = 'none';
}
}
function buildSubcatGrid() {
const grid = document.getElementById('subcatGrid');
grid.innerHTML = SUBCATEGORIES.map(s => `
<div class="subcat-item" id="subcat-${s.id}" onclick="selectSubcat(${s.id})">
<div class="num">${s.id}</div>
<div class="subcat-label">${s.name}</div>
</div>
`).join('');
}
function selectSubcat(id) {
state.currentSubcat = id;
document.querySelectorAll('.subcat-item').forEach(el => el.classList.remove('selected'));
document.getElementById('subcat-' + id).classList.add('selected');
}
// ============================================================
// RUBRIC
// ============================================================
function buildRubric() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
const container = document.getElementById('rubricContainer');
const sub = state.currentCategory === 'transformadores'
? `Proyecto: <strong>${document.getElementById('postulanteName').value}</strong> · Subcategoría: <strong>${SUBCATEGORIES.find(s=>s.id===state.currentSubcat).name}</strong>`
: `Postulante: <strong>${document.getElementById('postulanteName').value}</strong>`;
document.getElementById('panel3Sub').innerHTML = sub;
container.innerHTML = `<div class="rubric-section">
<div class="rubric-section-title">Criterios de evaluación</div>
${criteria.map(c => `
<div class="criterion-row">
<div class="criterion-info">
<div class="crit-name">${c.name}</div>
<div class="crit-desc">${c.desc}</div>
<span class="crit-weight">Peso: ${Math.round(c.weight*100)}%</span>
</div>
<div class="score-buttons">
${[1,2,3,4,5].map(n => `
<button class="score-btn" id="btn-${c.id}-${n}" onclick="setScore('${c.id}', ${n})">${n}</button>
`).join('')}
</div>
</div>
`).join('')}
</div>`;
document.getElementById('qualitativeSection').style.display =
state.currentCategory === 'vida' ? 'block' : 'none';
document.getElementById('q1').value = '';
document.getElementById('q2').value = '';
document.getElementById('observaciones').value = '';
updateScore();
}
function setScore(criterionId, score) {
state.scores[criterionId] = score;
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
const c = criteria.find(x => x.id === criterionId);
[1,2,3,4,5].forEach(n => {
const btn = document.getElementById(`btn-${criterionId}-${n}`);
if (btn) btn.classList.toggle('active', n === score);
});
updateScore();
}
function updateScore() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
let total = 0;
let filled = 0;
criteria.forEach(c => {
if (state.scores[c.id]) {
total += state.scores[c.id] * c.weight * 20; // scale to 100
filled++;
}
});
const display = document.getElementById('scoreDisplay');
const bar = document.getElementById('scoreBar');
const level = document.getElementById('scoreLevel');
if (filled === 0) {
display.textContent = '—';
bar.style.width = '0%';
level.textContent = 'Complete todos los criterios';
return;
}
const pct = filled === criteria.length ? total : (total / filled * criteria.reduce((a,c)=>a+(state.scores[c.id]?c.weight:0),0)) ;
// recalculate properly
let realTotal = 0;
criteria.forEach(c => { if (state.scores[c.id]) realTotal += state.scores[c.id] * c.weight * 20; });
const rounded = Math.round(realTotal * 10) / 10;
display.textContent = filled === criteria.length ? rounded : '...';
bar.style.width = (filled === criteria.length ? realTotal : 0) + '%';
const lvlText = realTotal >= 90 ? '🏆 Sobresaliente' : realTotal >= 75 ? '⭐ Alto' : realTotal >= 60 ? '✅ Aceptable' : realTotal >= 40 ? '⚠️ Bajo' : '❌ Muy bajo';
level.textContent = filled === criteria.length ? lvlText : `${filled}/${criteria.length} criterios calificados`;
}
// ============================================================
// SAVE
// ============================================================
function saveEvaluation() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
if (criteria.some(c => !state.scores[c.id])) { showToast('⚠️ Califique todos los criterios antes de guardar.'); return; }
let total = 0;
criteria.forEach(c => { total += state.scores[c.id] * c.weight * 20; });
total = Math.round(total * 10) / 10;
const evalObj = {
id: Date.now(),
fecha: new Date().toLocaleDateString('es-CO'),
hora: new Date().toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' }),
jurado: { ...state.jurado },
categoria: state.currentCategory,
subcategoria: state.currentCategory === 'transformadores'
? SUBCATEGORIES.find(s => s.id === state.currentSubcat).name : 'Reconocimiento a Toda una Vida',
postulante: document.getElementById('postulanteName').value.trim(),
institucion: document.getElementById('postulanteInstitucion').value.trim(),
scores: { ...state.scores },
total,
nivel: total >= 90 ? 'Sobresaliente' : total >= 75 ? 'Alto' : total >= 60 ? 'Aceptable' : total >= 40 ? 'Bajo' : 'Muy bajo',
q1: document.getElementById('q1').value,
q2: document.getElementById('q2').value,
observaciones: document.getElementById('observaciones').value,
};
state.evaluations.push(evalObj);
showToast('✅ Evaluación guardada correctamente');
goToStep(4);
renderSummary();
}
// ============================================================
// SUMMARY
// ============================================================
function renderSummary() {
const container = document.getElementById('evalListContainer');
const empty = document.getElementById('emptyState');
const tableWrap = document.getElementById('summaryTableWrap');
if (state.evaluations.length === 0) {
empty.style.display = 'block';
tableWrap.style.display = 'none';
container.innerHTML = '';
return;
}
empty.style.display = 'none';
tableWrap.style.display = 'block';
const tbody = document.getElementById('summaryBody');
tbody.innerHTML = state.evaluations.map((e, i) => `
<tr>
<td>${i + 1}</td>
<td><strong>${e.postulante}</strong></td>
<td>
<span class="tag ${e.categoria === 'transformadores' ? 'tag-transform' : 'tag-vida'}">
${e.categoria === 'transformadores' ? e.subcategoria : 'Toda una Vida'}
</span>
</td>
<td>${e.institucion || '—'}</td>
<td><strong>${e.total}</strong>/100</td>
<td>${e.nivel}</td>
</tr>
`).join('');
}
function updatePanel4Header() {
document.getElementById('panel4Sub').textContent =
`Jurado: ${state.jurado.nombre || '—'} · ${state.evaluations.length} evaluaciones registradas`;
}
function newEvaluation() {
state.currentCategory = null;
state.currentSubcat = null;
state.scores = {};
document.getElementById('postulanteName').value = '';
document.getElementById('postulanteInstitucion').value = '';
document.getElementById('cat-transform').classList.remove('selected');
document.getElementById('cat-vida').classList.remove('selected');
document.getElementById('subcatSection').style.display = 'none';
goToStep(2);
}
// ============================================================
// EXPORT TO EXCEL
// ============================================================
function exportToExcel() {
if (state.evaluations.length === 0) { showToast('⚠️ No hay evaluaciones para exportar.'); return; }
const wb = XLSX.utils.book_new();
// ---- Sheet 1: Resumen ----
const resumenData = [
['II JORNADA DE RECONOCIMIENTOS A BIBLIOTECAS TRANSFORMADORAS 2026'],
['Reporte de Evaluaciones de Jurado'],
['Generado:', new Date().toLocaleString('es-CO')],
['Jurado:', state.jurado.nombre, 'Código:', state.jurado.codigo],
['Institución:', state.jurado.institucion, 'Email:', state.jurado.email],
[],
['#', 'Fecha', 'Hora', 'Postulante / Proyecto', 'Institución', 'Categoría', 'Subcategoría',
'C1 Claridad Problema', 'C2 Innovación', 'C3 Impacto', 'C4 Sostenibilidad', 'C5 Gestión',
'V1 Trayectoria', 'V2 Impacto Inst.', 'V3 Aporte Sector', 'V4 Innovación/Legado',
'Puntaje Total (100)', 'Nivel', 'Q1 Contribución Principal', 'Q2 Justificación', 'Observaciones']
];
state.evaluations.forEach((e, i) => {
resumenData.push([
i + 1,
e.fecha,
e.hora,
e.postulante,
e.institucion,
e.categoria === 'transformadores' ? 'Proyectos Transformadores' : 'Reconocimiento a Toda una Vida',
e.subcategoria,
e.scores['c1'] || '',
e.scores['c2'] || '',
e.scores['c3'] || '',
e.scores['c4'] || '',
e.scores['c5'] || '',
e.scores['v1'] || '',
e.scores['v2'] || '',
e.scores['v3'] || '',
e.scores['v4'] || '',
e.total,
e.nivel,
e.q1 || '',
e.q2 || '',
e.observaciones || ''
]);
});
const ws1 = XLSX.utils.aoa_to_sheet(resumenData);
ws1['!cols'] = [
{wch:4},{wch:10},{wch:8},{wch:28},{wch:22},{wch:24},{wch:30},
{wch:8},{wch:10},{wch:8},{wch:12},{wch:10},
{wch:10},{wch:12},{wch:14},{wch:16},
{wch:14},{wch:12},{wch:35},{wch:35},{wch:30}
];
XLSX.utils.book_append_sheet(wb, ws1, 'Evaluaciones');
// ---- Sheet 2: Métricas ----
const transformEvals = state.evaluations.filter(e => e.categoria === 'transformadores');
const vidaEvals = state.evaluations.filter(e => e.categoria === 'vida');
const metricasData = [
['MÉTRICAS Y ESTADÍSTICAS'],
[],
['RESUMEN GENERAL'],
['Total evaluaciones', state.evaluations.length],
['Proyectos Transformadores', transformEvals.length],
['Reconocimiento Toda una Vida', vidaEvals.length],
[],
];
if (transformEvals.length > 0) {
const totales = transformEvals.map(e => e.total);
metricasData.push(['PROYECTOS TRANSFORMADORES — MÉTRICAS']);
metricasData.push(['Puntaje promedio', promedio(totales)]);
metricasData.push(['Puntaje máximo', Math.max(...totales)]);
metricasData.push(['Puntaje mínimo', Math.min(...totales)]);
metricasData.push([]);
metricasData.push(['Desglose por criterio (promedios)']);
metricasData.push(['Claridad del problema (15%)', promedioCol(transformEvals, 'c1')]);
metricasData.push(['Innovación y diferenciación (20%)', promedioCol(transformEvals, 'c2')]);
metricasData.push(['Impacto medible (25%)', promedioCol(transformEvals, 'c3')]);
metricasData.push(['Sostenibilidad y escalabilidad (20%)', promedioCol(transformEvals, 'c4')]);
metricasData.push(['Gestión y ejecución (20%)', promedioCol(transformEvals, 'c5')]);
metricasData.push([]);
metricasData.push(['Ranking Proyectos Transformadores']);
metricasData.push(['#', 'Postulante', 'Subcategoría', 'Institución', 'Puntaje', 'Nivel']);
[...transformEvals].sort((a,b) => b.total - a.total).forEach((e, i) =>
metricasData.push([i+1, e.postulante, e.subcategoria, e.institucion, e.total, e.nivel])
);
metricasData.push([]);
}
if (vidaEvals.length > 0) {
const totales = vidaEvals.map(e => e.total);
metricasData.push(['RECONOCIMIENTO TODA UNA VIDA — MÉTRICAS']);
metricasData.push(['Puntaje promedio', promedio(totales)]);
metricasData.push(['Puntaje máximo', Math.max(...totales)]);
metricasData.push(['Puntaje mínimo', Math.min(...totales)]);
metricasData.push([]);
metricasData.push(['Desglose por criterio (promedios)']);
metricasData.push(['Trayectoria y permanencia (20%)', promedioCol(vidaEvals, 'v1')]);
metricasData.push(['Impacto institucional y comunitario (25%)', promedioCol(vidaEvals, 'v2')]);
metricasData.push(['Aporte al desarrollo del sector (25%)', promedioCol(vidaEvals, 'v3')]);
metricasData.push(['Innovación y legado profesional (30%)', promedioCol(vidaEvals, 'v4')]);
metricasData.push([]);
metricasData.push(['Ranking Reconocimiento Toda una Vida']);
metricasData.push(['#', 'Postulante', 'Institución', 'Puntaje', 'Nivel']);
[...vidaEvals].sort((a,b) => b.total - a.total).forEach((e, i) =>
metricasData.push([i+1, e.postulante, e.institucion, e.total, e.nivel])
);
}
const ws2 = XLSX.utils.aoa_to_sheet(metricasData);
ws2['!cols'] = [{wch:40},{wch:20},{wch:25},{wch:22},{wch:12},{wch:15}];
XLSX.utils.book_append_sheet(wb, ws2, 'Métricas');
// ---- Export ----
const fecha = new Date().toISOString().slice(0,10);
const juradoSlug = (state.jurado.codigo || state.jurado.nombre || 'jurado').replace(/\s+/g,'-').toLowerCase();
XLSX.writeFile(wb, `evaluaciones-jurado-${juradoSlug}-${fecha}.xlsx`);
showToast('📥 Excel exportado exitosamente');
}
function promedio(arr) {
if (!arr.length) return 0;
return Math.round(arr.reduce((a,b)=>a+b,0)/arr.length * 10)/10;
}
function promedioCol(evals, key) {
const vals = evals.map(e => e.scores[key]).filter(Boolean);
return vals.length ? promedio(vals) : '—';
}
// ============================================================
// TOAST
// ============================================================
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
</script>
<script>
const SHEETS_URL = 'https://script.google.com/macros/s/AKfycbzIXGNogOSxI-TK5y8OeZeuXkh6OT1YTMK2AaXY1FQ_m5gShovL-rAsxLnUuyVMBZMd/exec';
// ============================================================
// DATA
// ============================================================
const SUBCATEGORIES = [
{ id: 1, name: "Innovación Tecnológica" },
{ id: 2, name: "Fomento a la Lectura Lúdica" },
{ id: 3, name: "Colaboración Interinstitucional" },
{ id: 4, name: "Impacto Social" },
{ id: 5, name: "Impacto Ambiental" },
{ id: 6, name: "Inclusión y Accesibilidad" },
{ id: 7, name: "Alfabetización Informacional" },
{ id: 8, name: "Diseño de Espacios Físicos" },
{ id: 9, name: "Uso Creativo de Recursos Bibliográficos" },
{ id: 10, name: "Red de Colaboración de Bibliotecas Universitarias" }
];
const CRITERIA_TRANSFORM = [
{ id: "c1", name: "Claridad del problema identificado", weight: 0.15, desc: "¿El proyecto identifica claramente el problema o necesidad que busca resolver?" },
{ id: "c2", name: "Innovación y diferenciación", weight: 0.20, desc: "¿Propone soluciones novedosas o enfoques diferenciadores respecto a prácticas tradicionales?" },
{ id: "c3", name: "Impacto medible", weight: 0.25, desc: "¿Se presentan evidencias cuantitativas o cualitativas del impacto generado?" },
{ id: "c4", name: "Sostenibilidad y escalabilidad", weight: 0.20, desc: "¿El proyecto puede mantenerse en el tiempo y replicarse en otros contextos?" },
{ id: "c5", name: "Gestión y ejecución del proyecto", weight: 0.20, desc: "¿Muestra evidencias de planificación, ejecución eficiente y logro de resultados?" },
];
const CRITERIA_VIDA = [
{ id: "v1", name: "Trayectoria y permanencia en el sector", weight: 0.20, desc: "Años de dedicación, continuidad y liderazgo sostenido en el ámbito bibliotecario o académico." },
{ id: "v2", name: "Impacto institucional y comunitario", weight: 0.25, desc: "Transformaciones generadas en bibliotecas o instituciones; influencia directa en comunidades académicas." },
{ id: "v3", name: "Aporte al desarrollo del sector", weight: 0.25, desc: "Participación en redes, producción académica, incidencia en políticas o modelos de gestión." },
{ id: "v4", name: "Innovación y legado profesional", weight: 0.30, desc: "Prácticas pioneras, modelos replicables, creación de metodologías o enfoques diferenciadores." },
];
// ============================================================
// STATE
// ============================================================
let state = {
currentStep: 1,
jurado: {},
currentCategory: null,
currentSubcat: null,
scores: {},
evaluations: [],
};
// ============================================================
// NAVIGATION
// ============================================================
function goToStep(n) {
for (let i = 1; i <= 4; i++) {
document.getElementById('panel' + i).classList.remove('visible');
const s = document.getElementById('step' + i);
s.classList.remove('active', 'done');
if (i < n) s.classList.add('done');
}
document.getElementById('panel' + n).classList.add('visible');
document.getElementById('step' + n).classList.add('active');
state.currentStep = n;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function continueToStep2() {
const nombre = document.getElementById('juradoNombre').value.trim();
if (!nombre) { showToast('⚠️ Ingrese su nombre completo.'); return; }
if (!document.getElementById('conflictoInteres').checked) { showToast('⚠️ Debe declarar el conflicto de interés.'); return; }
state.jurado = {
nombre,
codigo: document.getElementById('juradoCodigo').value.trim(),
institucion: document.getElementById('juradoInstitucion').value.trim(),
email: document.getElementById('juradoEmail').value.trim(),
};
updatePanel4Header();
goToStep(2);
}
function continueToStep3() {
if (!state.currentCategory) { showToast('⚠️ Seleccione una categoría.'); return; }
if (state.currentCategory === 'transformadores' && !state.currentSubcat) { showToast('⚠️ Seleccione la subcategoría.'); return; }
const name = document.getElementById('postulanteName').value.trim();
if (!name) { showToast('⚠️ Ingrese el nombre del proyecto o postulante.'); return; }
state.scores = {};
buildRubric();
goToStep(3);
}
// ============================================================
// CATEGORY SELECTION
// ============================================================
function selectCategory(cat) {
state.currentCategory = cat;
state.currentSubcat = null;
document.getElementById('cat-transform').classList.toggle('selected', cat === 'transformadores');
document.getElementById('cat-vida').classList.toggle('selected', cat === 'vida');
const subcatSection = document.getElementById('subcatSection');
if (cat === 'transformadores') {
subcatSection.style.display = 'block';
buildSubcatGrid();
} else {
subcatSection.style.display = 'none';
}
}
function buildSubcatGrid() {
const grid = document.getElementById('subcatGrid');
grid.innerHTML = SUBCATEGORIES.map(s => `
<div class="subcat-item" id="subcat-${s.id}" onclick="selectSubcat(${s.id})">
<div class="num">${s.id}</div>
<div class="subcat-label">${s.name}</div>
</div>
`).join('');
}
function selectSubcat(id) {
state.currentSubcat = id;
document.querySelectorAll('.subcat-item').forEach(el => el.classList.remove('selected'));
document.getElementById('subcat-' + id).classList.add('selected');
}
// ============================================================
// RUBRIC
// ============================================================
function buildRubric() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
const container = document.getElementById('rubricContainer');
const sub = state.currentCategory === 'transformadores'
? `Proyecto: <strong>${document.getElementById('postulanteName').value}</strong> · Subcategoría: <strong>${SUBCATEGORIES.find(s=>s.id===state.currentSubcat).name}</strong>`
: `Postulante: <strong>${document.getElementById('postulanteName').value}</strong>`;
document.getElementById('panel3Sub').innerHTML = sub;
container.innerHTML = `<div class="rubric-section">
<div class="rubric-section-title">Criterios de evaluación</div>
${criteria.map(c => `
<div class="criterion-row">
<div class="criterion-info">
<div class="crit-name">${c.name}</div>
<div class="crit-desc">${c.desc}</div>
<span class="crit-weight">Peso: ${Math.round(c.weight*100)}%</span>
</div>
<div class="score-buttons">
${[1,2,3,4,5].map(n => `
<button class="score-btn" id="btn-${c.id}-${n}" onclick="setScore('${c.id}', ${n})">${n}</button>
`).join('')}
</div>
</div>
`).join('')}
</div>`;
document.getElementById('qualitativeSection').style.display =
state.currentCategory === 'vida' ? 'block' : 'none';
document.getElementById('q1').value = '';
document.getElementById('q2').value = '';
document.getElementById('observaciones').value = '';
updateScore();
}
function setScore(criterionId, score) {
state.scores[criterionId] = score;
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
const c = criteria.find(x => x.id === criterionId);
[1,2,3,4,5].forEach(n => {
const btn = document.getElementById(`btn-${criterionId}-${n}`);
if (btn) btn.classList.toggle('active', n === score);
});
updateScore();
}
function updateScore() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
let total = 0;
let filled = 0;
criteria.forEach(c => {
if (state.scores[c.id]) {
total += state.scores[c.id] * c.weight * 20; // scale to 100
filled++;
}
});
const display = document.getElementById('scoreDisplay');
const bar = document.getElementById('scoreBar');
const level = document.getElementById('scoreLevel');
if (filled === 0) {
display.textContent = '—';
bar.style.width = '0%';
level.textContent = 'Complete todos los criterios';
return;
}
const pct = filled === criteria.length ? total : (total / filled * criteria.reduce((a,c)=>a+(state.scores[c.id]?c.weight:0),0)) ;
// recalculate properly
let realTotal = 0;
criteria.forEach(c => { if (state.scores[c.id]) realTotal += state.scores[c.id] * c.weight * 20; });
const rounded = Math.round(realTotal * 10) / 10;
display.textContent = filled === criteria.length ? rounded : '...';
bar.style.width = (filled === criteria.length ? realTotal : 0) + '%';
const lvlText = realTotal >= 90 ? '🏆 Sobresaliente' : realTotal >= 75 ? '⭐ Alto' : realTotal >= 60 ? '✅ Aceptable' : realTotal >= 40 ? '⚠️ Bajo' : '❌ Muy bajo';
level.textContent = filled === criteria.length ? lvlText : `${filled}/${criteria.length} criterios calificados`;
}
// ============================================================
// SAVE
// ============================================================
function saveEvaluation() {
const criteria = state.currentCategory === 'transformadores' ? CRITERIA_TRANSFORM : CRITERIA_VIDA;
if (criteria.some(c => !state.scores[c.id])) { showToast('⚠️ Califique todos los criterios antes de guardar.'); return; }
let total = 0;
criteria.forEach(c => { total += state.scores[c.id] * c.weight * 20; });
total = Math.round(total * 10) / 10;
const evalObj = {
id: Date.now(),
fecha: new Date().toLocaleDateString('es-CO'),
hora: new Date().toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' }),
jurado: { ...state.jurado },
categoria: state.currentCategory,
subcategoria: state.currentCategory === 'transformadores'
? SUBCATEGORIES.find(s => s.id === state.currentSubcat).name : 'Reconocimiento a Toda una Vida',
postulante: document.getElementById('postulanteName').value.trim(),
institucion: document.getElementById('postulanteInstitucion').value.trim(),
scores: { ...state.scores },
total,
nivel: total >= 90 ? 'Sobresaliente' : total >= 75 ? 'Alto' : total >= 60 ? 'Aceptable' : total >= 40 ? 'Bajo' : 'Muy bajo',
q1: document.getElementById('q1').value,
q2: document.getElementById('q2').value,
observaciones: document.getElementById('observaciones').value,
};
const row = {
'Fecha': evalObj.fecha, 'Hora': evalObj.hora,
'Jurado': evalObj.jurado.nombre, 'Código Jurado': evalObj.jurado.codigo,
'Institución Jurado': evalObj.jurado.institucion, 'Email Jurado': evalObj.jurado.email,
'Categoría': evalObj.categoria === 'transformadores' ? 'Proyectos Transformadores' : 'Reconocimiento a Toda una Vida',
'Subcategoría': evalObj.subcategoria,
'Postulante / Proyecto': evalObj.postulante, 'Institución Postulante': evalObj.institucion,
'C1 Claridad del problema (15%)': evalObj.scores['c1'] || '',
'C2 Innovación y diferenciación (20%)': evalObj.scores['c2'] || '',
'C3 Impacto medible (25%)': evalObj.scores['c3'] || '',
'C4 Sostenibilidad y escalabilidad (20%)': evalObj.scores['c4'] || '',
'C5 Gestión y ejecución (20%)': evalObj.scores['c5'] || '',
'V1 Trayectoria y permanencia (20%)': evalObj.scores['v1'] || '',
'V2 Impacto institucional y comunitario (25%)': evalObj.scores['v2'] || '',
'V3 Aporte al desarrollo del sector (25%)': evalObj.scores['v3'] || '',
'V4 Innovación y legado profesional (30%)': evalObj.scores['v4'] || '',
'Puntaje Total (100)': evalObj.total, 'Nivel': evalObj.nivel,
'Q1 - Contribución principal': evalObj.q1 || '',
'Q2 - Justificación reconocimiento': evalObj.q2 || '',
'Observaciones': evalObj.observaciones || '',
};
// Build GET query string
const params = Object.entries(row)
.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v))
.join('&');
const url = SHEETS_URL + '?' + params;
showToast('⏳ Enviando a Google Sheets...');
// Method 1: fetch no-cors
try {
fetch(url, { method: 'GET', mode: 'no-cors' })
.then(() => showToast('✅ Evaluación guardada en Google Sheets'))
.catch(() => {});
} catch(e) {}
// Method 2: fetch cors
try {
fetch(url, { method: 'GET', mode: 'cors' })
.then(() => {})
.catch(() => {});
} catch(e) {}
// Method 3: Image tag
try {
const img = new Image();
img.src = url;
} catch(e) {}
// Method 4: XMLHttpRequest
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
} catch(e) {}
// Method 5: script tag injection (most iframe-friendly)
try {
const script = document.createElement('script');
script.src = url + '&callback=_gsCallback';
document.head.appendChild(script);
setTimeout(() => { try { document.head.removeChild(script); } catch(e){} }, 3000);
} catch(e) {}
// Method 6: sendBeacon if available
try {
if (navigator.sendBeacon) {
navigator.sendBeacon(url);
}
} catch(e) {}
setTimeout(() => showToast('✅ Evaluación guardada en Google Sheets'), 1500);
state.evaluations.push(evalObj);
goToStep(4);
renderSummary();
}
// ============================================================
// SUMMARY
// ============================================================
function renderSummary() {
const container = document.getElementById('evalListContainer');
const empty = document.getElementById('emptyState');
const tableWrap = document.getElementById('summaryTableWrap');
if (state.evaluations.length === 0) {
empty.style.display = 'block';
tableWrap.style.display = 'none';
container.innerHTML = '';
return;
}
empty.style.display = 'none';
tableWrap.style.display = 'block';
const tbody = document.getElementById('summaryBody');
tbody.innerHTML = state.evaluations.map((e, i) => `
<tr>
<td>${i + 1}</td>
<td><strong>${e.postulante}</strong></td>
<td>
<span class="tag ${e.categoria === 'transformadores' ? 'tag-transform' : 'tag-vida'}">
${e.categoria === 'transformadores' ? e.subcategoria : 'Toda una Vida'}
</span>
</td>
<td>${e.institucion || '—'}</td>
<td><strong>${e.total}</strong>/100</td>
<td>${e.nivel}</td>
</tr>
`).join('');
}
function updatePanel4Header() {
document.getElementById('panel4Sub').textContent =
`Jurado: ${state.jurado.nombre || '—'} · ${state.evaluations.length} evaluaciones registradas`;
}
function newEvaluation() {
state.currentCategory = null;
state.currentSubcat = null;
state.scores = {};
document.getElementById('postulanteName').value = '';
document.getElementById('postulanteInstitucion').value = '';
document.getElementById('cat-transform').classList.remove('selected');
document.getElementById('cat-vida').classList.remove('selected');
document.getElementById('subcatSection').style.display = 'none';
goToStep(2);
}
// ============================================================
// EXPORT TO EXCEL
// ============================================================
function exportToExcel() {
if (state.evaluations.length === 0) { showToast('⚠️ No hay evaluaciones para exportar.'); return; }
const wb = XLSX.utils.book_new();
// ---- Sheet 1: Resumen ----
const resumenData = [
['II JORNADA DE RECONOCIMIENTOS A BIBLIOTECAS TRANSFORMADORAS 2026'],
['Reporte de Evaluaciones de Jurado'],
['Generado:', new Date().toLocaleString('es-CO')],
['Jurado:', state.jurado.nombre, 'Código:', state.jurado.codigo],
['Institución:', state.jurado.institucion, 'Email:', state.jurado.email],
[],
['#', 'Fecha', 'Hora', 'Postulante / Proyecto', 'Institución', 'Categoría', 'Subcategoría',
'C1 Claridad Problema', 'C2 Innovación', 'C3 Impacto', 'C4 Sostenibilidad', 'C5 Gestión',
'V1 Trayectoria', 'V2 Impacto Inst.', 'V3 Aporte Sector', 'V4 Innovación/Legado',
'Puntaje Total (100)', 'Nivel', 'Q1 Contribución Principal', 'Q2 Justificación', 'Observaciones']
];
state.evaluations.forEach((e, i) => {
resumenData.push([
i + 1,
e.fecha,
e.hora,
e.postulante,
e.institucion,
e.categoria === 'transformadores' ? 'Proyectos Transformadores' : 'Reconocimiento a Toda una Vida',
e.subcategoria,
e.scores['c1'] || '',
e.scores['c2'] || '',
e.scores['c3'] || '',
e.scores['c4'] || '',
e.scores['c5'] || '',
e.scores['v1'] || '',
e.scores['v2'] || '',
e.scores['v3'] || '',
e.scores['v4'] || '',
e.total,
e.nivel,
e.q1 || '',
e.q2 || '',
e.observaciones || ''
]);
});
const ws1 = XLSX.utils.aoa_to_sheet(resumenData);
ws1['!cols'] = [
{wch:4},{wch:10},{wch:8},{wch:28},{wch:22},{wch:24},{wch:30},
{wch:8},{wch:10},{wch:8},{wch:12},{wch:10},
{wch:10},{wch:12},{wch:14},{wch:16},
{wch:14},{wch:12},{wch:35},{wch:35},{wch:30}
];
XLSX.utils.book_append_sheet(wb, ws1, 'Evaluaciones');
// ---- Sheet 2: Métricas ----
const transformEvals = state.evaluations.filter(e => e.categoria === 'transformadores');
const vidaEvals = state.evaluations.filter(e => e.categoria === 'vida');
const metricasData = [
['MÉTRICAS Y ESTADÍSTICAS'],
[],
['RESUMEN GENERAL'],
['Total evaluaciones', state.evaluations.length],
['Proyectos Transformadores', transformEvals.length],
['Reconocimiento Toda una Vida', vidaEvals.length],
[],
];
if (transformEvals.length > 0) {
const totales = transformEvals.map(e => e.total);
metricasData.push(['PROYECTOS TRANSFORMADORES — MÉTRICAS']);
metricasData.push(['Puntaje promedio', promedio(totales)]);
metricasData.push(['Puntaje máximo', Math.max(...totales)]);
metricasData.push(['Puntaje mínimo', Math.min(...totales)]);
metricasData.push([]);
metricasData.push(['Desglose por criterio (promedios)']);
metricasData.push(['Claridad del problema (15%)', promedioCol(transformEvals, 'c1')]);
metricasData.push(['Innovación y diferenciación (20%)', promedioCol(transformEvals, 'c2')]);
metricasData.push(['Impacto medible (25%)', promedioCol(transformEvals, 'c3')]);
metricasData.push(['Sostenibilidad y escalabilidad (20%)', promedioCol(transformEvals, 'c4')]);
metricasData.push(['Gestión y ejecución (20%)', promedioCol(transformEvals, 'c5')]);
metricasData.push([]);
metricasData.push(['Ranking Proyectos Transformadores']);
metricasData.push(['#', 'Postulante', 'Subcategoría', 'Institución', 'Puntaje', 'Nivel']);
[...transformEvals].sort((a,b) => b.total - a.total).forEach((e, i) =>
metricasData.push([i+1, e.postulante, e.subcategoria, e.institucion, e.total, e.nivel])
);
metricasData.push([]);
}
if (vidaEvals.length > 0) {
const totales = vidaEvals.map(e => e.total);
metricasData.push(['RECONOCIMIENTO TODA UNA VIDA — MÉTRICAS']);
metricasData.push(['Puntaje promedio', promedio(totales)]);
metricasData.push(['Puntaje máximo', Math.max(...totales)]);
metricasData.push(['Puntaje mínimo', Math.min(...totales)]);
metricasData.push([]);
metricasData.push(['Desglose por criterio (promedios)']);
metricasData.push(['Trayectoria y permanencia (20%)', promedioCol(vidaEvals, 'v1')]);
metricasData.push(['Impacto institucional y comunitario (25%)', promedioCol(vidaEvals, 'v2')]);
metricasData.push(['Aporte al desarrollo del sector (25%)', promedioCol(vidaEvals, 'v3')]);
metricasData.push(['Innovación y legado profesional (30%)', promedioCol(vidaEvals, 'v4')]);
metricasData.push([]);
metricasData.push(['Ranking Reconocimiento Toda una Vida']);
metricasData.push(['#', 'Postulante', 'Institución', 'Puntaje', 'Nivel']);
[...vidaEvals].sort((a,b) => b.total - a.total).forEach((e, i) =>
metricasData.push([i+1, e.postulante, e.institucion, e.total, e.nivel])
);
}
const ws2 = XLSX.utils.aoa_to_sheet(metricasData);
ws2['!cols'] = [{wch:40},{wch:20},{wch:25},{wch:22},{wch:12},{wch:15}];
XLSX.utils.book_append_sheet(wb, ws2, 'Métricas');
// ---- Export ----
const fecha = new Date().toISOString().slice(0,10);
const juradoSlug = (state.jurado.codigo || state.jurado.nombre || 'jurado').replace(/\s+/g,'-').toLowerCase();
XLSX.writeFile(wb, `evaluaciones-jurado-${juradoSlug}-${fecha}.xlsx`);
showToast('📥 Excel exportado exitosamente');
}
function promedio(arr) {
if (!arr.length) return 0;
return Math.round(arr.reduce((a,b)=>a+b,0)/arr.length * 10)/10;
}
function promedioCol(evals, key) {
const vals = evals.map(e => e.scores[key]).filter(Boolean);
return vals.length ? promedio(vals) : '—';
}
// ============================================================
// TOAST
// ============================================================
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
</script>