mirror of
https://github.com/cursos-uai/sap_tfi_2026.git
synced 2026-09-17 17:29:21 -03:00
feat: generar informe markdown desde checklist
Co-authored-by: wsf <alejandro.sartorio@gmail.com> Signed-off-by: wsf <alejandro.sartorio@gmail.com>
This commit is contained in:
@@ -60,7 +60,7 @@ sap_tfi_2026/
|
||||
| **Prototipo ICONIX** | Explorar pantallas, acciones y trazabilidad `Boundary → Control → Entity`. | [Abrir prototipo](https://cursos-uai.github.io/sap_tfi_2026/) |
|
||||
| **Ejercicios interactivos** | Practicar fundamentos de diseño y los diagramas principales de ICONIX. | [Abrir ejercicios](https://cursos-uai.github.io/sap_tfi_2026/ejercicios.html) |
|
||||
| **Guías paso a paso** | Resolver casos de diseño con una hoja de ruta, pistas y soluciones modelo. | [Abrir guías](https://cursos-uai.github.io/sap_tfi_2026/guias.html) |
|
||||
| **Checklist del TP** | Controlar los 59 criterios, registrar evidencias y verificar la trazabilidad de la entrega. | [Abrir checklist](https://cursos-uai.github.io/sap_tfi_2026/checklist.html) |
|
||||
| **Checklist del TP** | Referenciar evidencias del borrador, generar el informe `.md` y preparar su envío al profesor. | [Abrir checklist](https://cursos-uai.github.io/sap_tfi_2026/checklist.html) |
|
||||
|
||||
Se recomienda comenzar por las **guías paso a paso** durante la elaboración del
|
||||
trabajo y utilizar la **checklist del TP** antes de cada revisión y de la
|
||||
|
||||
+4
-1
@@ -59,7 +59,10 @@ razonamiento, solicitar una pista y comparar al final con una solución modelo.
|
||||
Abrir `checklist.html` para controlar los diez grupos de entregables del TP:
|
||||
requerimientos, casos de uso, dominio, prototipo, arquitectura, robustez,
|
||||
secuencia, clases, patrones y testing estructural. Cada criterio admite una
|
||||
evidencia y el progreso queda guardado localmente en el navegador.
|
||||
referencia precisa al borrador —página, sección, tabla, figura, párrafo u otra—
|
||||
y el progreso queda guardado localmente en el navegador. Al finalizar se puede
|
||||
generar un informe `.md`, descargarlo y preparar el correo de seguimiento para
|
||||
el profesor Alejandro Sartorio.
|
||||
|
||||
## Alcance
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,9 +2,12 @@
|
||||
<body class="checklist-body"><header class="topbar"><a class="brand" href="index.html"><span class="brand-mark">S</span><span><strong>SAP · TFI</strong><small>Laboratorio ICONIX</small></span></a><nav class="course lab-links"><a href="index.html">Prototipo</a><a href="ejercicios.html">Ejercicios</a><a href="guias.html">Guías</a><a class="active" href="checklist.html">Checklist</a></nav></header>
|
||||
<main class="checklist-shell">
|
||||
<section class="checklist-hero"><div><p class="eyebrow">Control de entrega</p><h1>¿Tu TP está completo y es trazable?</h1><p>Marcá cada criterio sólo cuando puedas señalar su evidencia. El progreso se guarda en este navegador.</p></div><div class="hero-score"><strong id="total-percent">0%</strong><span id="total-copy">0 de 0 criterios</span></div></section>
|
||||
<section class="project-card" aria-labelledby="project-title"><div class="step-badge">Paso 1</div><div class="project-intro"><p class="eyebrow">Identificar la entrega</p><h2 id="project-title">Datos del trabajo</h2><p>Esta información encabezará el informe que recibirá el profesor.</p></div><div class="project-fields"><label>Alumno/a o equipo<input id="student-name" autocomplete="name" placeholder="Apellido, nombre o nombre del equipo"></label><label>Comisión<input id="course-group" placeholder="Ej.: SAP-TFI-2026 · Comisión A"></label><label class="wide">Link al documento borrador<input id="draft-link" type="url" inputmode="url" placeholder="https://docs.google.com/document/d/…"><small>Debe permitir al profesor abrir el documento.</small></label></div></section>
|
||||
<section class="quality-gates" aria-label="Hitos de revisión"><article><span>1</span><div><b>RRR</b><small>Requerimientos</small></div></article><i>→</i><article><span>2</span><div><b>PDR</b><small>Diseño preliminar</small></div></article><i>→</i><article><span>3</span><div><b>CDR</b><small>Diseño crítico</small></div></article><i>→</i><article><span>4</span><div><b>V&V</b><small>Testing y evidencias</small></div></article></section>
|
||||
<section class="toolbar"><label class="search-box"><span>⌕</span><input id="search" type="search" placeholder="Buscar criterio o artefacto…"></label><div class="filters"><button class="active" data-filter="all">Todos</button><button data-filter="pending">Pendientes</button><button data-filter="done">Completos</button></div><button id="expand-all" class="quiet-button">Contraer todo</button><button id="reset-checklist" class="quiet-button danger">Reiniciar</button></section>
|
||||
<div class="checklist-layout"><aside class="section-index"><p class="eyebrow">Entregables</p><nav id="section-index"></nav><div class="evidence-key"><b>Regla de oro</b><p>Un tilde sin archivo, página, diagrama, prueba o enlace verificable no cuenta como evidencia.</p></div></aside><section id="checklist-sections" class="checklist-sections" aria-live="polite"></section></div>
|
||||
<div class="checklist-layout"><aside class="section-index"><div class="step-badge">Paso 2</div><p class="eyebrow">Validar y referenciar</p><nav id="section-index"></nav><div class="evidence-key"><b>Regla de oro</b><p>Para completar un ítem indicá el tipo y la ubicación exacta de la evidencia dentro del borrador.</p></div></aside><section id="checklist-sections" class="checklist-sections" aria-live="polite"></section></div>
|
||||
<section class="submission-card"><div class="submission-copy"><div class="step-badge">Paso 3</div><p class="eyebrow">Preparar el seguimiento</p><h2>Generá y enviá el informe</h2><p>Revisá los datos, descargá el archivo Markdown y enviáselo al profesor para tutoría y evaluación.</p><div id="readiness" class="readiness" role="status"></div></div><div class="submission-actions"><button id="preview-report" class="quiet-button">Vista previa</button><button id="download-report" class="primary-action">Generar archivo .md</button><button id="email-report" class="email-action">Enviar al profesor</button><small>El correo se abrirá dirigido a <b>alejandro.sartorio@gmail.com</b>. Adjuntá el `.md` descargado antes de enviarlo.</small></div></section>
|
||||
<dialog id="report-dialog"><div class="dialog-head"><div><p class="eyebrow">Vista previa</p><h2>Informe de seguimiento</h2></div><button id="close-report" aria-label="Cerrar">×</button></div><pre id="report-preview"></pre></dialog>
|
||||
<section class="traceability-card"><div><p class="eyebrow">Chequeo transversal</p><h2>Cadena mínima de trazabilidad</h2><p>Elegí un caso de uso central y seguí la misma historia de extremo a extremo.</p></div><div class="trace-chain"><span>REQ</span><i>→</i><span>CU</span><i>→</i><span>UI</span><i>→</i><span>ROB</span><i>→</i><span>SEC</span><i>→</i><span>CLA</span><i>→</i><span>TEST</span></div></section>
|
||||
<footer class="checklist-sources"><b>Fuentes:</b><a href="https://docs.google.com/document/d/1oThnR9euP2YW4K94A3oAoE3pvHdeDyNShRdwWDY-Nmk/edit?usp=sharing" target="_blank" rel="noreferrer">Temas técnicos ↗</a><a href="https://docs.google.com/document/d/1_ZjiF3pUANt0-bm4V6Xph6hdJuHCnxbp/edit?usp=sharing" target="_blank" rel="noreferrer">Apunte ICONIX ↗</a><a href="https://www.fceia.unr.edu.ar/ingsoft/testing-intro-a.pdf" target="_blank" rel="noreferrer">Testing ↗</a><a href="https://www.fceia.unr.edu.ar/ingsoft/testing-estruct-a.pdf" target="_blank" rel="noreferrer">Testing estructural ↗</a><a href="https://www.fceia.unr.edu.ar/ingsoft/p-testing.pdf" target="_blank" rel="noreferrer">Práctica ↗</a></footer>
|
||||
</main><script src="checklist.js"></script></body></html>
|
||||
|
||||
+20
-8
@@ -9,12 +9,24 @@ const S=[
|
||||
['class','Diagrama de clases','Modelo estático listo para implementar',[['Clases con responsabilidades, atributos y operaciones','Las operaciones surgen de secuencia.'],['Tipos, visibilidad y firmas especificados','Los contratos permiten comprender la colaboración.'],['Relaciones y multiplicidades correctas','Herencia y composición tienen necesidad demostrable.'],['Alta cohesión y bajo acoplamiento revisados','Cada clase protege un secreto coherente.'],['Interfaces aíslan decisiones que pueden cambiar','Se aplica ocultamiento de información.'],['Consistencia con dominio, secuencias y código','No faltan ni sobran elementos sin trazabilidad.'],['Hito CDR aprobado','Diseño completo, correcto, realizable y alineado.']]],
|
||||
['patterns','Patrones de diseño','Problema, decisión y consecuencias',[['Cada patrón resuelve un problema concreto','Se explica contexto y necesidad.'],['Participantes y colaboraciones identificados','Las clases del TP se mapean a roles.'],['Clases reflejan la estructura del patrón','No es sólo un ejemplo genérico copiado.'],['Secuencias reflejan su comportamiento','Los mensajes muestran un escenario real.'],['Alternativas, beneficios y costos documentados','Evalúa acoplamiento, extensión y complejidad.']]],
|
||||
['test','Testing estructural','Pruebas derivadas del código',[['Unidad, especificación y dominio de entrada definidos','Cada caso usa valores concretos y salida esperada.'],['Grafo de flujo de control construido','Nodos, decisiones y flechas corresponden al código.'],['Criterio de cobertura declarado','Distingue sentencias, ramas, condiciones y caminos.'],['Conjunto de pruebas satisface el criterio','Vincula entrada, camino, esperado y obtenido.'],['Cobertura medida y evidencia adjunta','Incluye reporte o cálculo verificable.'],['Casos normales, límites y fallas incluidos','La selección busca revelar defectos.'],['Pruebas re-ejecutables y resultado registrado','Documenta ambiente, versión, fecha y estado.'],['Trazabilidad hasta TEST comprobada','Una prueba demuestra cada requisito del alcance.']]]];
|
||||
const K='sap-tfi-checklist-v1',root=document.querySelector('#checklist-sections'),index=document.querySelector('#section-index');let st=JSON.parse(localStorage.getItem(K)||'{}'),filter='all',query='';
|
||||
const items=S.flatMap(([sid,,,a])=>a.map((x,i)=>[`${sid}-${i}`, ...x]));
|
||||
const get=id=>st[id]||{done:false,evidence:''},esc=v=>(v||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),save=()=>localStorage.setItem(K,JSON.stringify(st));
|
||||
const K='sap-tfi-checklist-v2',OLD='sap-tfi-checklist-v1',META='sap-tfi-checklist-meta-v1';
|
||||
const root=document.querySelector('#checklist-sections'),index=document.querySelector('#section-index');
|
||||
let st=JSON.parse(localStorage.getItem(K)||localStorage.getItem(OLD)||'{}'),meta=JSON.parse(localStorage.getItem(META)||'{}'),filter='all',query='';
|
||||
const items=S.flatMap(([sid,,,a])=>a.map((x,i)=>[`${sid}-${i}`,...x]));
|
||||
const types=['Página','Sección','Título o subtítulo','Tabla','Figura o diagrama','Párrafo','Anexo','Link o archivo','Otro'];
|
||||
const get=id=>{const x=st[id]||{};return{done:!!x.done,type:x.type||'',location:x.location||x.evidence||''}};
|
||||
const esc=v=>(v||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const md=v=>(v||'').replace(/\|/g,'\\|').replace(/\r?\n/g,' ');
|
||||
function save(){localStorage.setItem(K,JSON.stringify(st));localStorage.setItem(META,JSON.stringify(meta))}
|
||||
function match(id,t,h){const x=get(id);return(filter==='all'||(filter==='done'?x.done:!x.done))&&`${t} ${h}`.toLowerCase().includes(query)}
|
||||
function render(){root.innerHTML=S.map(([sid,title,sub,a],n)=>{const done=a.filter((_,i)=>get(`${sid}-${i}`).done).length,vis=a.map((x,i)=>[`${sid}-${i}`,...x]).filter(x=>match(...x));return`<article class="check-section open" id="section-${sid}" ${vis.length?'':'hidden'}><button class="section-head" aria-expanded="true"><span class="section-number">${String(n+1).padStart(2,'0')}</span><span><h2>${title}</h2><p>${sub}</p></span><span class="section-progress"><b>${done}/${a.length}</b><small>completos</small></span><span class="chevron">⌃</span></button><div class="section-body">${vis.map(row).join('')}</div></article>`}).join('');if(!root.querySelector('.check-section:not([hidden])'))root.innerHTML='<div class="empty-state">No hay criterios que coincidan con este filtro.</div>';index.innerHTML=S.map(([sid,t,,a])=>`<a href="#section-${sid}"><span>${t}</span><b class="count">${a.filter((_,i)=>get(`${sid}-${i}`).done).length}/${a.length}</b></a>`).join('');bind();total()}
|
||||
function row([id,t,h]){const x=get(id);return`<div class="check-item ${x.done?'done':''}" data-id="${id}"><input id="${id}" type="checkbox" ${x.done?'checked':''}><div class="item-copy"><label for="${id}">${t}</label><p>${h}</p><input class="evidence-input" aria-label="Evidencia para ${t}" placeholder="Evidencia: archivo, página, URL, captura o reporte…" value="${esc(x.evidence)}"></div></div>`}
|
||||
function bind(){document.querySelectorAll('.section-head').forEach(b=>b.onclick=()=>{const c=b.closest('.check-section');c.classList.toggle('open');b.setAttribute('aria-expanded',c.classList.contains('open'))});document.querySelectorAll('.check-item').forEach(r=>{const id=r.dataset.id,c=r.querySelector('[type=checkbox]'),e=r.querySelector('.evidence-input');c.onchange=()=>{st[id]={done:c.checked,evidence:e.value};save();render()};e.oninput=()=>{st[id]={done:c.checked,evidence:e.value};save()}})}
|
||||
function total(){const done=items.filter(([id])=>get(id).done).length;document.querySelector('#total-percent').textContent=`${Math.round(done/items.length*100)}%`;document.querySelector('#total-copy').textContent=`${done} de ${items.length} criterios`}
|
||||
document.querySelector('#search').oninput=e=>{query=e.target.value.trim().toLowerCase();render()};document.querySelectorAll('[data-filter]').forEach(b=>b.onclick=()=>{filter=b.dataset.filter;document.querySelectorAll('[data-filter]').forEach(x=>x.classList.toggle('active',x===b));render()});document.querySelector('#expand-all').onclick=e=>{const a=[...document.querySelectorAll('.check-section')],open=a.some(c=>!c.classList.contains('open'));a.forEach(c=>c.classList.toggle('open',open));e.target.textContent=open?'Contraer todo':'Expandir todo'};document.querySelector('#reset-checklist').onclick=()=>{if(confirm('¿Borrar todos los tildes y evidencias?')){st={};save();render()}};render();
|
||||
function render(){root.innerHTML=S.map(([sid,title,sub,a],n)=>{const done=a.filter((_,i)=>get(`${sid}-${i}`).done).length,vis=a.map((x,i)=>[`${sid}-${i}`,...x]).filter(x=>match(...x));return`<article class="check-section open" id="section-${sid}" ${vis.length?'':'hidden'}><button class="section-head" aria-expanded="true"><span class="section-number">${String(n+1).padStart(2,'0')}</span><span><h2>${title}</h2><p>${sub}</p></span><span class="section-progress"><b>${done}/${a.length}</b><small>referenciados</small></span><span class="chevron">⌃</span></button><div class="section-body">${vis.map(row).join('')}</div></article>`}).join('');if(!root.querySelector('.check-section:not([hidden])'))root.innerHTML='<div class="empty-state">No hay criterios que coincidan con este filtro.</div>';index.innerHTML=S.map(([sid,t,,a])=>`<a href="#section-${sid}"><span>${t}</span><b class="count">${a.filter((_,i)=>get(`${sid}-${i}`).done).length}/${a.length}</b></a>`).join('');bind();total()}
|
||||
function row([id,t,h]){const x=get(id),invalid=x.done&&(!x.type||!x.location.trim());return`<div class="check-item ${x.done?'done':''} ${invalid?'invalid':''}" data-id="${id}"><input id="${id}" type="checkbox" ${x.done?'checked':''}><div class="item-copy"><label for="${id}">${t}</label><p>${h}</p><div class="evidence-fields"><select class="evidence-type" aria-label="Tipo de referencia para ${t}"><option value="">Tipo de referencia…</option>${types.map(v=>`<option ${x.type===v?'selected':''}>${v}</option>`).join('')}</select><input class="evidence-location" aria-label="Ubicación para ${t}" placeholder="Ej.: pág. 12, sección 3.2, figura D-ROB-01" value="${esc(x.location)}"></div><small class="reference-help">Indicá dónde puede encontrar el profesor la evidencia dentro del borrador.</small></div></div>`}
|
||||
function bind(){document.querySelectorAll('.section-head').forEach(b=>b.onclick=()=>{const c=b.closest('.check-section');c.classList.toggle('open');b.setAttribute('aria-expanded',c.classList.contains('open'))});document.querySelectorAll('.check-item').forEach(r=>{const id=r.dataset.id,c=r.querySelector('[type=checkbox]'),t=r.querySelector('.evidence-type'),l=r.querySelector('.evidence-location');const update=()=>{st[id]={done:c.checked,type:t.value,location:l.value};save();total()};c.onchange=()=>{update();render()};t.onchange=update;l.oninput=update})}
|
||||
function total(){const done=items.filter(([id])=>get(id).done).length,valid=items.filter(([id])=>{const x=get(id);return x.done&&x.type&&x.location.trim()}).length,missing=done-valid;document.querySelector('#total-percent').textContent=`${Math.round(valid/items.length*100)}%`;document.querySelector('#total-copy').textContent=`${valid} de ${items.length} referenciados`;const box=document.querySelector('#readiness');box.className=`readiness ${missing?'warning':valid?'ready':''}`;box.textContent=missing?`${missing} ítem${missing===1?'':'s'} marcado${missing===1?'':'s'} sin referencia completa.`:valid?`El informe tiene ${valid} evidencias listas para compartir.`:'Completá criterios para construir el informe.'}
|
||||
function syncMeta(){meta={student:document.querySelector('#student-name').value.trim(),group:document.querySelector('#course-group').value.trim(),draft:document.querySelector('#draft-link').value.trim()};save()}
|
||||
function report(){syncMeta();const done=items.filter(([id])=>get(id).done);let out=`# Informe de seguimiento del Trabajo Práctico\n\n- **Alumno/a o equipo:** ${md(meta.student)||'Sin completar'}\n- **Comisión:** ${md(meta.group)||'Sin completar'}\n- **Documento borrador:** ${meta.draft?`[Abrir borrador](${meta.draft})`:'Sin completar'}\n- **Fecha de generación:** ${new Date().toLocaleDateString('es-AR')}\n- **Avance referenciado:** ${done.filter(([id])=>get(id).type&&get(id).location.trim()).length}/${items.length} criterios\n\n`;S.forEach(([sid,title,,a])=>{out+=`## ${title}\n\n| Estado | Criterio | Tipo de referencia | Ubicación en el documento |\n|---|---|---|---|\n`;a.forEach(([t],i)=>{const x=get(`${sid}-${i}`);out+=`| ${x.done?'✅ Cumple':'⬜ Pendiente'} | ${md(t)} | ${md(x.type)||'—'} | ${md(x.location)||'—'} |\n`});out+='\n'});out+='---\n\nInforme generado con la checklist interactiva SAP-TFI-2026.\n';return out}
|
||||
function validateForShare(){syncMeta();const errors=[];if(!meta.student)errors.push('el nombre del alumno o equipo');if(!meta.draft||!/^https?:\/\//i.test(meta.draft))errors.push('un link válido al borrador');const incomplete=items.filter(([id])=>{const x=get(id);return x.done&&(!x.type||!x.location.trim())}).length;if(incomplete)errors.push(`la referencia de ${incomplete} ítem${incomplete===1?'':'s'} marcado${incomplete===1?'':'s'}`);if(errors.length){alert(`Antes de compartir, completá ${errors.join(', ')}.`);return false}return true}
|
||||
function download(){if(!validateForShare())return false;const blob=new Blob([report()],{type:'text/markdown;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a'),slug=(meta.student||'alumno').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');a.href=url;a.download=`seguimiento-tp-${slug||'alumno'}.md`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);return true}
|
||||
['student-name','course-group','draft-link'].forEach(id=>{const el=document.querySelector(`#${id}`),key=id==='student-name'?'student':id==='course-group'?'group':'draft';el.value=meta[key]||'';el.oninput=syncMeta});
|
||||
document.querySelector('#search').oninput=e=>{query=e.target.value.trim().toLowerCase();render()};document.querySelectorAll('[data-filter]').forEach(b=>b.onclick=()=>{filter=b.dataset.filter;document.querySelectorAll('[data-filter]').forEach(x=>x.classList.toggle('active',x===b));render()});document.querySelector('#expand-all').onclick=e=>{const a=[...document.querySelectorAll('.check-section')],open=a.some(c=>!c.classList.contains('open'));a.forEach(c=>c.classList.toggle('open',open));e.target.textContent=open?'Contraer todo':'Expandir todo'};document.querySelector('#reset-checklist').onclick=()=>{if(confirm('¿Borrar todos los datos, tildes y referencias?')){st={};meta={};localStorage.removeItem(K);localStorage.removeItem(META);location.reload()}};
|
||||
const dialog=document.querySelector('#report-dialog');document.querySelector('#preview-report').onclick=()=>{document.querySelector('#report-preview').textContent=report();dialog.showModal()};document.querySelector('#close-report').onclick=()=>dialog.close();document.querySelector('#download-report').onclick=download;document.querySelector('#email-report').onclick=()=>{if(!download())return;const subject=encodeURIComponent(`Seguimiento TP SAP-TFI-2026 · ${meta.student}`),body=encodeURIComponent(`Profesor Alejandro Sartorio:\n\nAdjunto el informe Markdown de seguimiento de nuestro Trabajo Práctico.\n\nDocumento borrador: ${meta.draft}\n\nAlumno/a o equipo: ${meta.student}\nComisión: ${meta.group||'Sin completar'}\n\nImportante: adjuntar el archivo .md que acaba de descargarse antes de enviar este correo.`);window.location.href=`mailto:alejandro.sartorio@gmail.com?subject=${subject}&body=${body}`};render();
|
||||
|
||||
Reference in New Issue
Block a user