mirror of
https://github.com/cursos-uai/sap_tfi_2026.git
synced 2026-09-17 17:29:21 -03:00
Compare commits
18
Commits
6c8f468b71
...
8d0f1b5433
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0f1b5433 | ||
|
|
4aa1374ddc | ||
|
|
49cdc3364c | ||
|
|
19f0883a70 | ||
|
|
4738cff39e | ||
|
|
988386bf60 | ||
|
|
ed151a6074 | ||
|
|
e04b30b33a | ||
|
|
d8815d1fc2 | ||
|
|
f96428cf3e | ||
|
|
0c55a7ce82 | ||
|
|
3ac7618047 | ||
|
|
9f77b4f8e3 | ||
|
|
ecdc98fe39 | ||
|
|
095a15b42d | ||
|
|
3712648cd3 | ||
|
|
20adf4716f | ||
|
|
8faf0b65b9 |
@@ -0,0 +1,47 @@
|
||||
name: Publicar prototipo en GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "prototipo/**"
|
||||
- "diagrams/svg/**"
|
||||
- ".github/workflows/pages.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Descargar repositorio
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Preparar sitio
|
||||
run: |
|
||||
mkdir -p _site/diagrams/svg
|
||||
cp -R prototipo/. _site/
|
||||
cp diagrams/svg/*.svg _site/diagrams/svg/
|
||||
|
||||
- name: Configurar Pages
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Empaquetar sitio
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: _site
|
||||
|
||||
- name: Publicar
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -47,11 +47,99 @@ sap_tfi_2026/
|
||||
│ └── pdf/ ← versiones PDF
|
||||
├── evidence/
|
||||
│ └── index.md ← catálogo de evidencias
|
||||
├── prototipo/ ← pantallas navegables y trazabilidad ICONIX
|
||||
├── trabajo_final/ ← entregas de alumnos por pull request
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recursos interactivos para el Trabajo Práctico
|
||||
|
||||
| Recurso | Para qué sirve | Abrir |
|
||||
|---------|----------------|-------|
|
||||
| **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** | 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
|
||||
entrega final.
|
||||
|
||||
---
|
||||
|
||||
## Procedimiento de entrega final
|
||||
|
||||
Antes de rendir, cada alumno debe entregar el trabajo mediante GitHub:
|
||||
|
||||
1. Hacer un [fork de este repositorio](https://github.com/cursos-uai/sap_tfi_2026/fork).
|
||||
2. Subir el trabajo completo dentro de
|
||||
**`trabajo_final/Apellido_Nombre/`**, reemplazando el ejemplo por los datos
|
||||
del alumno y sin utilizar espacios.
|
||||
3. Abrir un **pull request** desde el fork hacia la rama `main` de este
|
||||
repositorio.
|
||||
4. Presentar cada actualización posterior mediante un **nuevo pull request**.
|
||||
|
||||
Las instrucciones completas y la estructura esperada están en
|
||||
[`trabajo_final/README.md`](trabajo_final/README.md). No se deben modificar las
|
||||
carpetas correspondientes a otros alumnos.
|
||||
|
||||
---
|
||||
|
||||
## Prototipo navegable de fronteras ICONIX
|
||||
|
||||
El directorio **`prototipo/`** contiene una representación interactiva de las
|
||||
pantallas que actúan como fronteras (*boundaries*) en los diagramas de robustez.
|
||||
Permite observar cómo los botones, formularios, tablas, mensajes y otros
|
||||
componentes de interfaz se relacionan con las acciones de control y con los
|
||||
datos del dominio.
|
||||
|
||||
### Escenarios incluidos
|
||||
|
||||
| Pantalla | Caso de uso | Diagrama de robustez | Acciones representadas |
|
||||
|----------|-------------|-----------------------|------------------------|
|
||||
| Presupuesto | `CU-VEN-001` | `D-ROB-VEN-001` | Guardar, enviar por email, cancelar y agregar productos |
|
||||
| Confirmación | `CU-VEN-004` | `D-ROB-VEN-004` | Validar y confirmar el pedido |
|
||||
| Reserva | `CU-ENT-002` | `D-ROB-ENT-002` | Comprobar disponibilidad y reservar productos |
|
||||
| Facturación | `CU-FAC-001` | `D-ROB-FAC-001` | Seleccionar modalidad y crear una factura borrador |
|
||||
|
||||
### Relación con ICONIX
|
||||
|
||||
Cada escenario dispone de dos vistas:
|
||||
|
||||
- **Pantalla**: simula la interacción del actor con botones, campos,
|
||||
formularios, tablas, estados y mensajes.
|
||||
- **Trazabilidad**: vincula cada elemento visible con la terna
|
||||
**Boundary → Control → Entity**.
|
||||
|
||||
| Elemento ICONIX | Representación en el prototipo | Ejemplo |
|
||||
|-----------------|----------------------------------|---------|
|
||||
| **Boundary** | Componente visible o interactivo | Botón **Confirmar**, formulario del pedido |
|
||||
| **Control** | Acción disparada por la interfaz | `action_confirm()` |
|
||||
| **Entity** | Modelo consultado o modificado | `sale.order`, `stock.picking` |
|
||||
|
||||
Al ejecutar una acción, el panel inferior informa qué control se invoca y
|
||||
qué entidades intervienen. Desde la vista **Trazabilidad** también se puede
|
||||
abrir el diagrama de robustez SVG asociado.
|
||||
|
||||
### Uso local
|
||||
|
||||
El prototipo es estático y no requiere instalar dependencias. Se puede abrir
|
||||
directamente con `prototipo/index.html` o servir desde la raíz del repositorio:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000 --directory prototipo
|
||||
```
|
||||
|
||||
Luego visitar <http://localhost:8000>.
|
||||
|
||||
> **Alcance:** es una herramienta didáctica con datos ficticios. No está
|
||||
> conectada a una instancia de Odoo ni pretende reproducir exactamente todas
|
||||
> sus vistas, permisos o reglas de visibilidad.
|
||||
|
||||
---
|
||||
|
||||
## Cómo usar este repositorio para el TP
|
||||
|
||||
### Paso 1: Entender la metodología ICONIX
|
||||
@@ -332,4 +420,4 @@ Material de lectura recomendado para profundizar en la metodología ICONIX y su
|
||||
|
||||
## Licencia
|
||||
|
||||
Este material se distribuye bajo la licencia Creative Commons BY-SA 4.0. Podés usarlo, modificarlo y redistribuirlo libremente, citando la fuente.
|
||||
Este material se distribuye bajo la licencia Creative Commons BY-SA 4.0. Podés usarlo, modificarlo y redistribuirlo libremente, citando la fuente.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Prototipo de fronteras ICONIX
|
||||
|
||||
Prototipo navegable que representa las pantallas asociadas a los diagramas de
|
||||
robustez del flujo comercial. Su objetivo es mostrar, con fines didácticos, la
|
||||
trazabilidad entre:
|
||||
|
||||
- **Boundary**: pantalla, formulario, botón, enlace, tabla o mensaje visible.
|
||||
- **Control**: acción disparada por una interacción.
|
||||
- **Entity**: modelo y datos consultados o modificados.
|
||||
|
||||
## Abrir
|
||||
|
||||
No requiere instalación ni servidor. Abrir `index.html` en un navegador.
|
||||
|
||||
La versión publicada está disponible en:
|
||||
<https://cursos-uai.github.io/sap_tfi_2026/>.
|
||||
|
||||
Para servirlo localmente (opcional):
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000 --directory prototipo
|
||||
```
|
||||
|
||||
Luego visitar <http://localhost:8000>.
|
||||
|
||||
## Escenarios incluidos
|
||||
|
||||
| Pantalla | Caso de uso | Diagrama de robustez |
|
||||
|---|---|---|
|
||||
| Presupuesto | CU-VEN-001 | D-ROB-VEN-001 |
|
||||
| Confirmación | CU-VEN-004 | D-ROB-VEN-004 |
|
||||
| Reserva | CU-ENT-002 | D-ROB-ENT-002 |
|
||||
| Facturación | CU-FAC-001 | D-ROB-FAC-001 |
|
||||
|
||||
## Ejercicios interactivos
|
||||
|
||||
Abrir `ejercicios.html` para recorrer seis estaciones de aprendizaje:
|
||||
|
||||
1. fundamentos de diseño para el cambio y ocultamiento de información;
|
||||
2. modelo de dominio;
|
||||
3. casos de uso;
|
||||
4. diagramas de robustez;
|
||||
5. diagramas de secuencia;
|
||||
6. diagramas de clases.
|
||||
|
||||
Los ejercicios ofrecen corrección inmediata, explicación de la respuesta y
|
||||
progreso persistente en el navegador. La base teórica se encuentra citada al
|
||||
pie de cada estación.
|
||||
|
||||
## Guías paso a paso
|
||||
|
||||
Abrir `guias.html` para resolver seis casos progresivos con una hoja de ruta:
|
||||
comprender el problema, anticipar cambios, descomponer, diseñar interfaces,
|
||||
establecer relaciones y validar el diseño. Cada paso permite registrar el
|
||||
razonamiento, solicitar una pista y comparar al final con una solución modelo.
|
||||
|
||||
## Checklist del Trabajo Práctico
|
||||
|
||||
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
|
||||
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. Para la entrega definitiva antes de rendir, la
|
||||
página también guía el fork del repositorio, la ubicación obligatoria
|
||||
`trabajo_final/Apellido_Nombre/` y el pull request que registra cada versión.
|
||||
|
||||
## Alcance
|
||||
|
||||
Es un prototipo educativo, no una reproducción exacta ni conectada a Odoo. Los
|
||||
datos son ficticios y las acciones se ejecutan en memoria. Las correspondencias
|
||||
se basan en las fuentes PlantUML existentes en `diagrams/plantuml/`.
|
||||
@@ -0,0 +1,83 @@
|
||||
const scenarios = {
|
||||
presupuesto: {
|
||||
title: "Crear presupuesto", diagram: "D-ROB-VEN-001", state: "BORRADOR",
|
||||
actions: [["Guardar", "primary", "save"], ["Enviar por email", "", "email"], ["Cancelar", "", "cancel"]],
|
||||
trace: [
|
||||
["Botón Nuevo + formulario", "Crea y actualiza el presupuesto", "sale.order"],
|
||||
["Campos cliente y líneas", "Calcula importes", "res.partner + sale.order.line"],
|
||||
["Botón Enviar por email", "action_quotation_send()", "sale.order + mail.mail"]
|
||||
]
|
||||
},
|
||||
confirmacion: {
|
||||
title: "Confirmar pedido", diagram: "D-ROB-VEN-004", state: "PRESUPUESTO ENVIADO",
|
||||
actions: [["Confirmar", "primary", "confirm"], ["Volver al presupuesto", "", "back"]],
|
||||
trace: [
|
||||
["Formulario + botón Confirmar", "action_confirm()", "sale.order"],
|
||||
["Mensaje de validación", "_confirmation_error_message()", "sale.order + sale.order.line"],
|
||||
["Resumen de entrega", "_action_confirm()", "stock.picking"],
|
||||
["Opción de factura", "_create_invoices()", "account.move"]
|
||||
]
|
||||
},
|
||||
reserva: {
|
||||
title: "Reservar productos", diagram: "D-ROB-ENT-002", state: "ESPERANDO RESERVA",
|
||||
actions: [["Comprobar disponibilidad", "primary", "reserve"], ["Ver reglas", "", "rules"]],
|
||||
trace: [
|
||||
["Botón Comprobar disponibilidad", "action_assign()", "stock.picking"],
|
||||
["Tabla de disponibilidad", "Busca y reserva cantidad", "stock.move.line"],
|
||||
["Aviso de faltante", "Ejecuta reabastecimiento", "stock.warehouse.orderpoint + purchase.order"]
|
||||
]
|
||||
},
|
||||
factura: {
|
||||
title: "Crear factura", diagram: "D-ROB-FAC-001", state: "PARA FACTURAR",
|
||||
actions: [["Crear factura", "primary", "invoice"], ["Cancelar", "", "cancel"]],
|
||||
trace: [
|
||||
["Botón Crear factura", "_create_invoices()", "sale.order"],
|
||||
["Opciones de facturación", "Valida líneas facturables", "sale.order.line"],
|
||||
["Vista previa de datos", "Calcula y crea factura", "account.move + account.move.line + account.tax"]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const money = new Intl.NumberFormat("es-AR", {style:"currency", currency:"ARS", maximumFractionDigits:0});
|
||||
const lines = [{product:"Notebook Pro 14", qty:2, price:210000, discount:0, tax:"IVA 21%"},{product:"Dock USB-C", qty:3, price:22000, discount:5, tax:"IVA 21%"}];
|
||||
let activeScenario = "presupuesto";
|
||||
|
||||
function renderLines(){
|
||||
const body=document.querySelector("#order-lines");
|
||||
body.innerHTML=lines.map((line,i)=>`<tr><td>${line.product}</td><td>${line.qty}</td><td>${money.format(line.price)}</td><td>${line.discount}%</td><td>${line.tax}</td><td>${money.format(line.qty*line.price*(1-line.discount/100))}</td></tr>`).join("");
|
||||
const untaxed=lines.reduce((sum,line)=>sum+line.qty*line.price*(1-line.discount/100),0);
|
||||
document.querySelector("#untaxed").textContent=money.format(untaxed);
|
||||
document.querySelector("#tax").textContent=money.format(untaxed*.21);
|
||||
document.querySelector("#total").textContent=money.format(untaxed*1.21);
|
||||
}
|
||||
|
||||
function renderScenario(key){
|
||||
activeScenario=key; const scenario=scenarios[key];
|
||||
document.querySelectorAll(".nav-item").forEach(item=>item.classList.toggle("active",item.dataset.scenario===key));
|
||||
document.querySelectorAll(".screen").forEach(screen=>screen.classList.toggle("active",screen.id===`screen-${key}`));
|
||||
document.querySelector("#screen-title").textContent=scenario.title;
|
||||
document.querySelector("#diagram-id").textContent=scenario.diagram;
|
||||
document.querySelector("#record-state").textContent=scenario.state;
|
||||
document.querySelector("#primary-actions").innerHTML=scenario.actions.map(([label,kind,action])=>`<button class="${kind}" data-action="${action}">${label}</button>`).join("");
|
||||
document.querySelector("#trace-grid").innerHTML=scenario.trace.map(row=>`<article class="trace-row"><div class="trace-cell boundary-cell"><span>Boundary</span><strong>${row[0]}</strong><p>Elemento que ve o utiliza el actor.</p></div><div class="trace-cell control-cell"><span>Control</span><strong>${row[1]}</strong><p>Comportamiento disparado.</p></div><div class="trace-cell entity-cell"><span>Entity</span><strong>${row[2]}</strong><p>Datos consultados o modificados.</p></div></article>`).join("");
|
||||
const diagramBase=location.pathname.includes("/prototipo/")?"../diagrams/svg/":"diagrams/svg/";
|
||||
document.querySelector("#diagram-link").href=`${diagramBase}${encodeURIComponent(scenario.diagram)}%20${encodeURIComponent("—")}%20${diagramFilename(key)}.svg`;
|
||||
log("Pantalla preparada",`Estás explorando ${scenario.diagram}. Usá los botones para simular el flujo.`);
|
||||
}
|
||||
|
||||
function diagramFilename(key){return {presupuesto:"Diagrama%20de%20Robustez:%20Crear%20Presupuesto",confirmacion:"Diagrama%20de%20Robustez:%20Confirmar%20Pedido",reserva:"Diagrama%20de%20Robustez:%20Reservar%20Productos",factura:"Diagrama%20de%20Robustez:%20Crear%20Factura"}[key]}
|
||||
function log(title,detail){document.querySelector("#event-title").textContent=title;document.querySelector("#event-detail").textContent=detail}
|
||||
|
||||
document.querySelector("#scenario-nav").addEventListener("click",event=>{const item=event.target.closest("[data-scenario]");if(item)renderScenario(item.dataset.scenario)});
|
||||
document.querySelector(".view-switch").addEventListener("click",event=>{const button=event.target.closest("[data-view]");if(!button)return;document.querySelectorAll(".view-switch button").forEach(b=>b.classList.toggle("active",b===button));document.querySelectorAll(".view").forEach(v=>v.classList.toggle("active",v.id===`${button.dataset.view}-view`))});
|
||||
document.querySelector("#primary-actions").addEventListener("click",event=>{
|
||||
const action=event.target.dataset.action;if(!action)return;
|
||||
const messages={save:["Presupuesto guardado","Boundary formulario → actualiza entity sale.order."],email:["Correo preparado","Boundary Enviar → action_quotation_send() → mail.mail."],cancel:["Acción cancelada","No se modificaron entidades en esta simulación."],back:["Volver al presupuesto","Navegación hacia view_sale_order_form."],rules:["Reglas consultadas","Control de reabastecimiento → stock.warehouse.orderpoint."],confirm:["Pedido confirmado","action_confirm() cambió sale.order y generó stock.picking."],reserve:["Reserva procesada","action_assign() reservó 2 unidades y detectó disponibilidad parcial."],invoice:["Factura borrador creada","_create_invoices() → account.move + account.move.line."]};
|
||||
if(action==="confirm")document.querySelector("#record-state").textContent="PEDIDO CONFIRMADO";
|
||||
if(action==="reserve"){document.querySelector("#reserved-a").textContent="2";document.querySelector("#reserved-b").textContent="1";document.querySelector("#reserve-state-a").textContent="Reservado";document.querySelector("#reserve-state-a").className="pill ok";document.querySelector("#reserve-state-b").textContent="Parcial";document.querySelector("#reserve-state-b").className="pill partial";document.querySelector("#record-state").textContent="PARCIALMENTE DISPONIBLE"}
|
||||
if(action==="invoice")document.querySelector("#record-state").textContent="FACTURA BORRADOR";
|
||||
log(...messages[action]);
|
||||
});
|
||||
document.querySelector("#add-line").addEventListener("click",()=>{lines.push({product:"Monitor 27 pulgadas",qty:1,price:185000,discount:0,tax:"IVA 21%"});renderLines();log("Línea agregada","El formulario actualiza sale.order.line y recalcula los importes de sale.order.")});
|
||||
|
||||
renderLines();renderScenario(activeScenario);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
<!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Checklist interactiva para la entrega del Trabajo Práctico con ICONIX"><title>Checklist del TP · SAP-TFI-2026</title><link rel="stylesheet" href="styles.css"><link rel="stylesheet" href="checklist.css"></head>
|
||||
<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"><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>
|
||||
<section class="final-delivery" aria-labelledby="delivery-title"><header><div class="step-badge">Paso 4 · Entrega final</div><p class="eyebrow">Antes de rendir</p><h2 id="delivery-title">Publicá tu trabajo mediante un pull request</h2><p>La entrega oficial queda registrada en GitHub. Repetí el pull request cada vez que presentes una actualización.</p></header><ol class="delivery-steps"><li><span>1</span><div><h3>Hacé un fork del repositorio</h3><p>Creá una copia en tu cuenta desde <a href="https://github.com/cursos-uai/sap_tfi_2026/fork" target="_blank" rel="noreferrer">cursos-uai/sap_tfi_2026 ↗</a>.</p><label><input id="delivery-fork" type="checkbox"> Ya creé mi fork</label></div></li><li><span>2</span><div><h3>Subí el trabajo en la ubicación obligatoria</h3><p>Usá exactamente <code>trabajo_final/Apellido_Nombre/</code>, reemplazando el ejemplo por tus datos y sin espacios.</p><label>Nombre de la carpeta<input id="delivery-folder" placeholder="Ej.: Sartorio_Alejandro"></label></div></li><li><span>3</span><div><h3>Abrí un pull request hacia este repositorio</h3><p>En el título indicá apellido, nombre y versión. Para cada actualización, agregá cambios a tu fork y abrí un nuevo PR.</p><label>Link del pull request<input id="delivery-pr" type="url" placeholder="https://github.com/cursos-uai/sap_tfi_2026/pull/…"></label></div></li></ol><div id="delivery-status" class="delivery-status" role="status"></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>
|
||||
@@ -0,0 +1,33 @@
|
||||
const S=[
|
||||
['req','Requerimientos','Capacidades y restricciones',[['Funcionales identificados, numerados y sin decisiones de implementación','Cada requisito expresa una capacidad observable.'],['No funcionales clasificados','Considerar seguridad, desempeño, usabilidad, portabilidad y disponibilidad.'],['Alcance, actores y glosario acordados','El vocabulario es consistente en todos los artefactos.'],['Cada requisito se vincula con casos de uso','La matriz permite recorrer REQ → CU.'],['Al menos un criterio de aceptación por requisito','Debe ser comprobable y describir el resultado esperado.']]],
|
||||
['cu','Casos de uso y especificaciones','Comportamiento desde la mirada del actor',[['Diagrama general con actores, casos y límite del sistema','Cada actor persigue un objetivo claro.'],['include y extend usados con sentido','include es obligatorio reutilizado; extend, opcional o condicionado.'],['Una especificación por caso relevante','Incluye objetivo, actor, precondiciones, disparador y postcondiciones.'],['Curso básico escrito en voz activa','Cada paso muestra actor, acción y respuesta del sistema.'],['Alternativos, excepciones y reglas documentados','Cada desvío tiene condición, respuesta y cierre.'],['Hito RRR revisado con interesados','Casos de uso, dominio y prototipo capturan los requerimientos.']]],
|
||||
['dom','Modelo de dominio','Vocabulario conceptual del problema',[['Clases tomadas del lenguaje del negocio','No incluye pantallas, controles ni detalles técnicos.'],['Atributos conceptuales relevantes','Evita claves foráneas y decisiones de persistencia.'],['Relaciones y multiplicidades justificadas','Cada asociación expresa un hecho del dominio.'],['Nombres sin sinónimos ni ambigüedades','El mismo concepto conserva su nombre.'],['Modelo actualizado después de robustez','Todas las entidades descubiertas aparecen en el dominio.']]],
|
||||
['ui','Prototipo','Fronteras y recorrido del usuario',[['Pantallas y estados relevantes representados','Incluye formularios, listados, mensajes, confirmaciones y errores.'],['Cada pantalla se vincula con casos y actores','La navegación materializa cursos básicos y alternativos.'],['Controles y datos coinciden con las especificaciones','No hay acciones huérfanas ni entradas faltantes.'],['Prototipo revisado con usuarios','Se registraron observaciones y decisiones.'],['Se usa como especificación, no como producto final','Sirve para desambiguar y descubrir fronteras.']]],
|
||||
['arch','Arquitectura','Estructura y decisiones técnicas',[['Vistas funcional, de datos y del sistema separadas','Cada una responde una pregunta arquitectónica.'],['Componentes, capas, responsabilidades e interfaces visibles','Las dependencias respetan el estilo declarado.'],['Despliegue, nodos y tecnologías documentados','Incluye persistencia e integraciones externas.'],['Decisiones justificadas por requerimientos','No se basan sólo en preferencias tecnológicas.'],['Calidad y testing considerados','Contempla cambio, seguridad, rendimiento y comprobabilidad.']]],
|
||||
['rob','Diagramas de robustez','Puente entre casos de uso y diseño',[['Un diagrama por caso de uso diseñado','El texto se sigue frase por frase.'],['Actor, Boundary, Control y Entity diferenciados','Fronteras y entidades son sustantivos; controles, acciones.'],['Reglas de comunicación respetadas','Actor↔Boundary; Boundary↔Control; Entity↔Control.'],['Una frontera clara por pantalla o interfaz','Los nombres coinciden con prototipo y texto.'],['Entidades consistentes con el dominio','Toda entidad está en el modelo actualizado.'],['Controles cubren cursos básicos y alternativos','Sin asignar todavía métodos ni algoritmos.'],['Hito PDR aprobado','Texto, robustez, dominio, datos y arquitectura son consistentes.']]],
|
||||
['seq','Diagramas de secuencia','Diseño detallado por escenario',[['Un diagrama por caso de uso','Incluye curso básico y alternativos.'],['Participantes derivados de robustez','No aparecen objetos sin responsabilidad explicada.'],['Texto del caso mapeado a mensajes','Orden, retornos, condiciones y ciclos son visibles.'],['Controles asignados a operaciones u objetos','Cada mensaje llega al responsable del comportamiento.'],['Mensajes respetan arquitectura y patrones','El detalle orienta la implementación.'],['Errores y resultados observables representados','Las respuestas regresan a la frontera.']]],
|
||||
['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-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>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(),fork:document.querySelector('#delivery-fork').checked,folder:document.querySelector('#delivery-folder').value.trim(),pr:document.querySelector('#delivery-pr').value.trim()};save();deliveryStatus()}
|
||||
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+=`## Entrega final por GitHub\n\n- **Fork creado:** ${meta.fork?'Sí':'Pendiente'}\n- **Carpeta de entrega:** ${meta.folder?`\`trabajo_final/${md(meta.folder)}/\``:'Pendiente'}\n- **Pull request:** ${meta.pr?`[Abrir pull request](${meta.pr})`:'Pendiente'}\n- **Política de actualización:** abrir un nuevo pull request por cada actualización de la entrega.\n\n---\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});const fork=document.querySelector('#delivery-fork'),folder=document.querySelector('#delivery-folder'),pr=document.querySelector('#delivery-pr');fork.checked=!!meta.fork;folder.value=meta.folder||'';pr.value=meta.pr||'';fork.onchange=syncMeta;folder.oninput=syncMeta;pr.oninput=syncMeta;
|
||||
function deliveryStatus(){const box=document.querySelector('#delivery-status'),folderOk=/^[A-Za-zÀ-ÿ'’-]+_[A-Za-zÀ-ÿ'’-]+$/.test(meta.folder||''),prOk=/^https:\/\/github\.com\/[^/]+\/sap_tfi_2026\/pull\/\d+/i.test(meta.pr||'');const count=[meta.fork,folderOk,prOk].filter(Boolean).length;box.className=`delivery-status ${count===3?'ready':''}`;box.textContent=count===3?'Entrega final documentada: fork, carpeta y pull request completos.':`${count}/3 pasos documentados. La carpeta debe usar Apellido_Nombre y el link debe corresponder a un pull request.`}
|
||||
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}`};deliveryStatus();render();
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Laboratorio interactivo para aprender diseño de software e ICONIX">
|
||||
<title>Ejercicios ICONIX · SAP-TFI-2026</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="ejercicios.css">
|
||||
</head>
|
||||
<body class="lab-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 class="active" href="ejercicios.html">Ejercicios</a><a href="guias.html">Guías</a><a href="checklist.html">Checklist</a></nav>
|
||||
</header>
|
||||
|
||||
<main class="lab-layout">
|
||||
<aside class="lesson-nav">
|
||||
<div><p class="eyebrow">Tu recorrido</p><h2>Del cambio a las clases</h2><div class="progress-meter"><i id="progress-bar"></i></div><small id="progress-copy">0 de 6 estaciones completadas</small></div>
|
||||
<nav id="lesson-list"></nav>
|
||||
<button id="reset-progress" class="reset-button">Reiniciar progreso</button>
|
||||
</aside>
|
||||
|
||||
<section class="lesson-stage">
|
||||
<header class="lesson-header"><div><p class="eyebrow" id="lesson-kicker">Fundamentos</p><h1 id="lesson-title">Diseñar para el cambio</h1><p id="lesson-summary"></p></div><span id="lesson-score" class="score-chip">0 / 0</span></header>
|
||||
<div id="lesson-content"></div>
|
||||
<footer class="lesson-footer"><button id="previous-lesson">← Anterior</button><button id="next-lesson" class="primary">Siguiente →</button></footer>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="completion-dialog"><div class="celebration">✦</div><h2>Recorrido completado</h2><p>Ya conectaste fundamentos de diseño con los cinco artefactos principales de ICONIX.</p><button id="close-dialog">Seguir practicando</button></dialog>
|
||||
<script src="ejercicios.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
const lessons=[
|
||||
{id:"fundamentos",kicker:"Fundamentos",title:"Diseñar para el cambio",summary:"Antes de dibujar clases hay que decidir por qué se divide el sistema y qué cambio intenta aislar cada parte.",theory:{title:"Idea central",text:"Diseñar es descomponer el software, asignar funciones y establecer relaciones. El criterio no es producir muchas partes: es reducir el costo de los cambios sin degradar la integridad conceptual.",concepts:[["Cambio probable","Anticipar qué requisito, algoritmo, dato o tecnología puede variar."],["Secreto del módulo","Cada módulo aísla una decisión de diseño con alta probabilidad de cambio."],["Interfaz estable","Los clientes usan servicios abstractos y no dependen de la implementación."]]},exercises:[
|
||||
{type:"choice",q:"Una empresa puede cambiar el proveedor de pagos varias veces. ¿Qué decisión sigue mejor a Parnas?",options:["Incluir condiciones del proveedor en Pedido","Aislar la integración tras una interfaz de pagos","Exponer el SDK del proveedor a todas las clases"],answer:1,why:"El proveedor es un ítem de cambio. Una interfaz estable evita propagar ese cambio."},
|
||||
{type:"choice",q:"¿Cuál es el objetivo principal de un buen diseño?",options:["Usar la mayor cantidad posible de clases","Minimizar el costo de cambio y preservar la integridad conceptual","Copiar directamente el modelo funcional"],answer:1,why:"La cantidad de clases no es el criterio; importa la descomposición frente al cambio."}
|
||||
],source:"Apuntes Una Teoría para el Diseño de Software y Diseño de Software, secciones sobre Diseño para el Cambio y DBOI."},
|
||||
{id:"dominio",kicker:"ICONIX · Modelo de dominio",title:"Encontrar conceptos, no soluciones",summary:"El modelo de dominio establece el vocabulario conceptual antes de introducir detalles técnicos.",theory:{title:"Regla de trabajo",text:"Extraé conceptos del problema, sus atributos significativos y relaciones. No confundas entidades del dominio con pantallas, controladores o tablas de una tecnología particular.",concepts:[["Concepto","Objeto reconocible del mundo del problema."],["Atributo","Propiedad significativa del concepto."],["Relación","Vínculo semántico con cardinalidad."]]},exercises:[
|
||||
{type:"multi",q:"Seleccioná sólo conceptos del dominio de ventas.",options:["Cliente","PedidoVenta","Botón Confirmar","PostgreSQL","Producto"],answer:[0,1,4],why:"Cliente, PedidoVenta y Producto pertenecen al dominio; el botón y PostgreSQL son detalles de solución."},
|
||||
{type:"choice",q:"¿Cuál relación expresa mejor el dominio?",options:["PedidoVenta 1 contiene 1..* LíneaPedido","Form llama a SQL","Controlador importa ORM"],answer:0,why:"La primera relación usa conceptos y cardinalidad sin contaminar el modelo conceptual."}
|
||||
],source:"Proceso ICONIX del repositorio: modelo de dominio conceptual y separación entre requerimientos y diseño."},
|
||||
{id:"casos",kicker:"ICONIX · Casos de uso",title:"Narrar comportamiento observable",summary:"Los casos de uso conectan objetivos de actores con respuestas del sistema y preparan el análisis de robustez.",theory:{title:"Del objetivo al flujo",text:"Nombrá el caso de uso con un verbo, identificá actor y objetivo, y redactá pasos en voz activa. Usá include para comportamiento obligatorio reutilizado y extend para comportamiento condicional.",concepts:[["Actor","Rol externo que persigue un objetivo."],["include","Subflujo obligatorio compartido."],["extend","Comportamiento opcional bajo condición."]]},exercises:[
|
||||
{type:"choice",q:"Enviar el presupuesto por email es opcional. ¿Qué relación corresponde?",options:["include","extend","generalización de actor"],answer:1,why:"El envío aparece sólo si el vendedor decide realizarlo."},
|
||||
{type:"sort",q:"Ordená el camino feliz de Crear presupuesto.",items:["El sistema calcula los totales","El vendedor selecciona el cliente","El vendedor guarda el presupuesto","El vendedor agrega productos"],answer:["El vendedor selecciona el cliente","El vendedor agrega productos","El sistema calcula los totales","El vendedor guarda el presupuesto"],why:"El orden conserva causa y efecto y luego podrá trazarse al diagrama de robustez."}
|
||||
],source:"Casos de uso CU-VEN-001 y D-CU-GEN-002 del repositorio SAP-TFI-2026."},
|
||||
{id:"robustez",kicker:"ICONIX · Robustez",title:"Separar frontera, control y entidad",summary:"El análisis de robustez verifica que el texto del caso de uso pueda convertirse en colaboraciones de objetos.",theory:{title:"Tres responsabilidades",text:"El actor interactúa con fronteras; las fronteras invocan controles; los controles coordinan entidades. Evitá que la interfaz manipule directamente la persistencia o que una entidad asuma decisiones de presentación.",concepts:[["Boundary","Pantalla, formulario, API o mensaje visible."],["Control","Coordinador de un flujo o regla de aplicación."],["Entity","Concepto persistente con estado del dominio."]]},exercises:[
|
||||
{type:"classify",q:"Clasificá cada elemento del flujo Confirmar pedido.",items:[["Botón Confirmar","Boundary"],["action_confirm()","Control"],["sale.order","Entity"]],options:["Boundary","Control","Entity"],why:"La clasificación mantiene separadas interacción, coordinación y estado."},
|
||||
{type:"choice",q:"¿Qué interacción revela mayor acoplamiento indebido?",options:["Actor → Formulario","Formulario → Control","Formulario → PostgreSQL"],answer:2,why:"La frontera conoce un detalle de persistencia y saltea el control y las entidades."}
|
||||
],source:"Diagramas D-ROB del repositorio y principio de bajo acoplamiento del apunte Diseño de Software."},
|
||||
{id:"secuencia",kicker:"ICONIX · Secuencia",title:"Asignar mensajes en el tiempo",summary:"El diagrama de secuencia refina el flujo de robustez y asigna operaciones concretas a participantes.",theory:{title:"Trazabilidad temporal",text:"Cada mensaje debe derivarse de un paso del caso de uso y de una interacción de robustez. Las alternativas se representan con condiciones explícitas y las operaciones se asignan al objeto que posee la responsabilidad.",concepts:[["Lifeline","Participante que existe durante la interacción."],["Mensaje","Solicitud de un servicio en orden temporal."],["alt / opt","Variación condicional del flujo."]]},exercises:[
|
||||
{type:"sort",q:"Ordená los mensajes de Confirmar pedido.",items:["Stock crea picking","Formulario invoca action_confirm()","Control valida pedido","sale.order cambia a sale"],answer:["Formulario invoca action_confirm()","Control valida pedido","sale.order cambia a sale","Stock crea picking"],why:"Primero se recibe la intención, luego se valida y sólo después se modifican entidades."},
|
||||
{type:"choice",q:"Si la validación puede fallar, ¿qué fragmento UML corresponde?",options:["loop","alt / else","par"],answer:1,why:"alt / else muestra caminos mutuamente excluyentes: éxito o error."}
|
||||
],source:"Diagramas D-SEC del repositorio y trazabilidad definida por el proceso ICONIX."},
|
||||
{id:"clases",kicker:"ICONIX · Diagrama de clases",title:"Construir clases que oculten decisiones",summary:"El modelo de clases asigna datos, operaciones y relaciones sin exponer decisiones que probablemente cambien.",theory:{title:"De Parnas a UML",text:"Una clase no se justifica sólo porque exista un sustantivo: debe reunir una responsabilidad coherente y proteger un secreto. La interfaz pública debe ser pequeña, abstracta e insensible a cambios anticipados.",concepts:[["Alta cohesión","Los elementos internos contribuyen a un único secreto o responsabilidad."],["Bajo acoplamiento","Las clases conocen la menor cantidad necesaria de otras clases."],["Encapsulamiento","Los parámetros y retornos no revelan la representación interna."]]},exercises:[
|
||||
{type:"choice",q:"Un catálogo puede pasar de arreglo a base de datos. ¿Cuál interfaz oculta mejor esa decisión?",options:["Producto[] getArray()","Producto buscarPorId(IdProducto id)","Producto get(int posicion)"],answer:1,why:"Buscar por identidad expresa una necesidad del dominio sin exponer arreglo, posición ni capacidad."},
|
||||
{type:"builder",q:"Construí un diseño para descuentos variables.",tokens:["Pedido","PoliticaDescuento","BaseDatosDescuento","calcularTotal()","aplicarA(Pedido)","getFila(int)"],answer:["Pedido","PoliticaDescuento","calcularTotal()","aplicarA(Pedido)"],why:"Pedido compone una Política de Descuento mediante servicios del dominio; la tabla y el acceso por fila quedan ocultos."}
|
||||
],source:"Diseño de Software, secciones DBOI, interfaz/implementación, abstracción, encapsulamiento, cohesión/acoplamiento y diseño orientado a objetos."}
|
||||
];
|
||||
|
||||
let lessonIndex=0;
|
||||
let progress=JSON.parse(localStorage.getItem("iconix-progress")||"{}");
|
||||
const selections={};
|
||||
|
||||
function saveProgress(){localStorage.setItem("iconix-progress",JSON.stringify(progress))}
|
||||
function allCorrect(lesson){return lesson.exercises.every((_,i)=>progress[`${lesson.id}-${i}`])}
|
||||
function renderNav(){
|
||||
document.querySelector("#lesson-list").innerHTML=lessons.map((lesson,i)=>`<button class="lesson-button ${i===lessonIndex?"active":""} ${allCorrect(lesson)?"done":""}" data-index="${i}"><span>${allCorrect(lesson)?"✓":i+1}</span><strong>${lesson.title}</strong><small>${lesson.kicker.replace("ICONIX · ","")}</small></button>`).join("");
|
||||
const completed=lessons.filter(allCorrect).length;
|
||||
document.querySelector("#progress-bar").style.width=`${completed/lessons.length*100}%`;
|
||||
document.querySelector("#progress-copy").textContent=`${completed} de ${lessons.length} estaciones completadas`;
|
||||
}
|
||||
function renderLesson(){
|
||||
const lesson=lessons[lessonIndex];
|
||||
document.querySelector("#lesson-kicker").textContent=lesson.kicker;document.querySelector("#lesson-title").textContent=lesson.title;document.querySelector("#lesson-summary").textContent=lesson.summary;
|
||||
document.querySelector("#lesson-content").innerHTML=`<article class="theory-card"><p class="eyebrow">${lesson.theory.title}</p><p>${lesson.theory.text}</p></article><div class="concept-grid">${lesson.theory.concepts.map(c=>`<article class="concept-card"><b>${c[0]}</b><p>${c[1]}</p></article>`).join("")}</div>${lesson.exercises.map((e,i)=>exerciseHTML(e,i,lesson.id)).join("")}<p class="source-note"><b>Base conceptual:</b> ${lesson.source} Fuentes: <a href="https://www.fceia.unr.edu.ar/ingsoft/intro-diseno.pdf" target="_blank" rel="noreferrer">Introducción al diseño ↗</a> · <a href="https://www.fceia.unr.edu.ar/ingsoft/diseno-a.pdf" target="_blank" rel="noreferrer">Diseño y ocultamiento de información ↗</a></p>`;
|
||||
document.querySelector("#previous-lesson").disabled=lessonIndex===0;document.querySelector("#next-lesson").textContent=lessonIndex===lessons.length-1?"Finalizar ✓":"Siguiente →";
|
||||
updateScore();renderNav();
|
||||
}
|
||||
function exerciseHTML(e,i,id){
|
||||
const common=`<header><div><h3>${i+1}. ${e.q}</h3></div><span class="exercise-type">${({choice:"Una respuesta",multi:"Selección múltiple",sort:"Ordenar",classify:"Clasificar",builder:"Construir"})[e.type]}</span></header>`;
|
||||
let body="";
|
||||
if(e.type==="choice"||e.type==="multi")body=`<div class="options">${e.options.map((o,n)=>`<button class="option" data-option="${n}"><span>${e.type==="multi"?"□":"○"}</span>${o}</button>`).join("")}</div>`;
|
||||
if(e.type==="sort")body=`<div class="sort-list">${e.items.map((item,n)=>`<div class="sort-item" data-value="${item}"><i>${n+1}</i><span>${item}</span><div class="sort-controls"><button data-move="up">↑</button><button data-move="down">↓</button></div></div>`).join("")}</div>`;
|
||||
if(e.type==="classify")body=`<div class="options">${e.items.map((item,n)=>`<label class="option">${item[0]}<select data-classify="${n}"><option value="">Elegir…</option>${e.options.map(o=>`<option>${o}</option>`).join("")}</select></label>`).join("")}</div>`;
|
||||
if(e.type==="builder")body=`<div class="builder"><div class="palette"><h4>Elementos disponibles</h4>${e.tokens.map((t,n)=>`<button class="token" data-token="${n}">${t}</button>`).join("")}</div><div class="canvas"><h4>Tu diagrama</h4><div class="uml-preview" data-preview><p>Elegí clases y operaciones.</p></div></div></div>`;
|
||||
return `<article class="exercise-card" data-exercise="${i}" data-type="${e.type}" data-lesson="${id}">${common}${body}<button class="check-button" data-check>Comprobar</button><div class="feedback"></div></article>`
|
||||
}
|
||||
function updateScore(){const lesson=lessons[lessonIndex];const score=lesson.exercises.filter((_,i)=>progress[`${lesson.id}-${i}`]).length;document.querySelector("#lesson-score").textContent=`${score} / ${lesson.exercises.length}`}
|
||||
function checkExercise(card){
|
||||
const lesson=lessons[lessonIndex],i=Number(card.dataset.exercise),e=lesson.exercises[i];let correct=false;
|
||||
if(e.type==="choice")correct=Number(card.querySelector(".option.selected")?.dataset.option)===e.answer;
|
||||
if(e.type==="multi"){const got=[...card.querySelectorAll(".option.selected")].map(x=>Number(x.dataset.option)).sort();correct=JSON.stringify(got)===JSON.stringify(e.answer)}
|
||||
if(e.type==="sort")correct=JSON.stringify([...card.querySelectorAll(".sort-item")].map(x=>x.dataset.value))===JSON.stringify(e.answer);
|
||||
if(e.type==="classify")correct=[...card.querySelectorAll("select")].every((x,n)=>x.value===e.items[n][1]);
|
||||
if(e.type==="builder"){const got=selections[`${lesson.id}-${i}`]||[];correct=e.answer.every(x=>got.includes(x))&&got.every(x=>e.answer.includes(x))}
|
||||
const feedback=card.querySelector(".feedback");feedback.className=`feedback visible ${correct?"success":"error"}`;feedback.textContent=`${correct?"✓ Correcto.":"Todavía no."} ${e.why}`;
|
||||
progress[`${lesson.id}-${i}`]=correct;saveProgress();updateScore();renderNav();
|
||||
}
|
||||
document.querySelector("#lesson-content").addEventListener("click",event=>{
|
||||
const card=event.target.closest(".exercise-card");if(!card)return;
|
||||
if(event.target.closest("[data-option]")){const option=event.target.closest("[data-option]");if(card.dataset.type==="choice")card.querySelectorAll(".option").forEach(o=>o.classList.remove("selected"));option.classList.toggle("selected")}
|
||||
const mover=event.target.closest("[data-move]");if(mover){const row=mover.closest(".sort-item"),sibling=mover.dataset.move==="up"?row.previousElementSibling:row.nextElementSibling;if(sibling)row.parentElement.insertBefore(mover.dataset.move==="up"?row:sibling,mover.dataset.move==="up"?sibling:row)}
|
||||
const token=event.target.closest("[data-token]");if(token){const key=`${lessons[lessonIndex].id}-${card.dataset.exercise}`,value=lessons[lessonIndex].exercises[card.dataset.exercise].tokens[token.dataset.token];selections[key]=selections[key]||[];selections[key]=selections[key].includes(value)?selections[key].filter(x=>x!==value):[...selections[key],value];token.classList.toggle("active");card.querySelector("[data-preview]").innerHTML=builderPreview(selections[key])}
|
||||
if(event.target.closest("[data-check]"))checkExercise(card);
|
||||
});
|
||||
function builderPreview(items){const classes=items.filter(x=>!x.includes("(")&&!x.startsWith("Base"));const ops=items.filter(x=>x.includes("("));if(!items.length)return"<p>Elegí clases y operaciones.</p>";return classes.map((c,i)=>`<div class="uml-box"><strong>${c}</strong><div>${ops[i]||"+ interfaz abstracta"}</div></div>`).join('<div class="relationship">◇── usa ──▷</div>')}
|
||||
document.querySelector("#lesson-list").addEventListener("click",e=>{const b=e.target.closest("[data-index]");if(b){lessonIndex=Number(b.dataset.index);renderLesson()}});
|
||||
document.querySelector("#previous-lesson").addEventListener("click",()=>{if(lessonIndex>0){lessonIndex--;renderLesson();scrollTo(0,0)}});
|
||||
document.querySelector("#next-lesson").addEventListener("click",()=>{if(lessonIndex<lessons.length-1){lessonIndex++;renderLesson();scrollTo(0,0)}else document.querySelector("#completion-dialog").showModal()});
|
||||
document.querySelector("#reset-progress").addEventListener("click",()=>{progress={};saveProgress();renderLesson()});document.querySelector("#close-dialog").addEventListener("click",()=>document.querySelector("#completion-dialog").close());
|
||||
renderLesson();
|
||||
@@ -0,0 +1,2 @@
|
||||
.guide-body{min-height:100vh}.guide-shell{max-width:1250px;margin:auto;padding:38px clamp(16px,4vw,52px) 60px}.guide-hero{display:flex;justify-content:space-between;align-items:end;gap:25px}.guide-hero h1{font:700 clamp(34px,5vw,58px)/1 Georgia,serif;margin:0}.guide-hero>div:first-child>p:last-child{max-width:760px;color:var(--muted);font-size:17px}.guide-progress{background:#172433;color:white;border-radius:14px;padding:18px 25px;min-width:150px}.guide-progress strong{font:700 27px Georgia,serif;display:block}.guide-progress span{color:#b7c1cb;font-size:12px}.case-picker{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin:28px 0}.case-button{border:1px solid var(--line);background:white;border-radius:10px;padding:12px;text-align:left;cursor:pointer;color:var(--ink)}.case-button span{display:block;color:var(--muted);font-size:10px;margin-bottom:3px}.case-button.active{background:#172433;border-color:#172433;color:white}.case-button.done:not(.active){border-color:#5ab492;background:#edfaf5}.case-workspace{display:grid;grid-template-columns:245px 1fr;background:white;border:1px solid var(--line);border-radius:16px;overflow:hidden;box-shadow:var(--shadow)}.roadmap{background:#f5f7f8;padding:25px;border-right:1px solid var(--line)}.roadmap ol{padding:0;margin:0;list-style:none;display:grid;gap:6px}.roadmap li{display:grid;grid-template-columns:25px 1fr;padding:9px;border-radius:8px;color:var(--muted);font-size:12px}.roadmap li i{font-style:normal}.roadmap li.active{background:white;color:var(--blue);font-weight:800}.roadmap li.done{color:#087658}.step-panel{padding:28px min(5vw,55px);min-height:560px}.step-panel>header{display:flex;justify-content:space-between;align-items:start}.step-panel h2{font:700 32px Georgia,serif;margin:0}.step-panel>header>span{background:#edf1f4;padding:7px 10px;border-radius:18px;color:var(--muted);font-size:11px;font-weight:800}.problem-box{background:#172433;color:#d4dde5;border-radius:11px;padding:18px 20px;margin:20px 0}.problem-box b{color:white}.step-question h3{font-size:21px;margin-bottom:5px}.step-question>p{color:var(--muted);margin-top:0}.answer-area{width:100%;min-height:135px;border:1px solid var(--line);border-radius:9px;padding:14px;font:14px/1.5 ui-monospace,monospace;resize:vertical}.checklist{display:grid;gap:8px;margin:16px 0}.checklist label{display:flex;gap:9px;padding:10px;border:1px solid var(--line);border-radius:8px}.hint-box{display:none;background:#fff5df;border-left:4px solid #e19b27;padding:13px 15px;margin-top:13px;color:#704b13}.hint-box.visible{display:block}.step-actions{display:flex;gap:9px;margin-top:18px}.step-actions button,.solution-card button,dialog button{border:1px solid var(--line);background:white;padding:10px 14px;border-radius:8px;font-weight:800;cursor:pointer}.step-actions .primary,dialog button{margin-left:auto;background:var(--blue);border-color:var(--blue);color:white}.solution-card{border:1px solid #bce3d3;background:#effaf6;border-radius:10px;padding:18px}.solution-card h3{margin-top:0}.solution-card ul{padding-left:20px}.method-card{margin:28px 0;background:white;border:1px solid var(--line);border-radius:14px;padding:25px}.method-card h2{font:700 27px Georgia,serif}.method-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.method-grid span{background:#f5f7f8;padding:13px;border-radius:8px}.method-grid b{display:inline-grid;place-items:center;width:22px;height:22px;background:var(--blue);color:white;border-radius:50%;margin-right:8px;font-size:11px}.guide-sources{display:flex;gap:15px;flex-wrap:wrap;color:var(--muted);font-size:13px}.guide-sources a{color:var(--blue)}dialog{border:0;border-radius:15px;padding:30px;max-width:650px;box-shadow:0 25px 80px rgba(0,0,0,.3)}dialog::backdrop{background:rgba(15,25,35,.68)}.solution-mark{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;background:#dff5eb;color:#087658;font-weight:900}dialog h2{font:700 28px Georgia,serif}.model-modules{display:grid;grid-template-columns:repeat(2,1fr);gap:9px}.model-module{border:1px solid var(--line);border-radius:8px;padding:12px}.model-module strong{display:block;color:var(--blue)}.model-module small{color:var(--muted)}
|
||||
@media(max-width:900px){.guide-hero{align-items:start}.case-picker{grid-template-columns:repeat(3,1fr)}.case-workspace{grid-template-columns:1fr}.roadmap{border-right:0;border-bottom:1px solid var(--line)}.roadmap ol{display:flex;overflow:auto}.roadmap li{min-width:140px}.method-grid{grid-template-columns:1fr 1fr}}@media(max-width:600px){.guide-shell{padding-top:24px}.guide-hero{display:block}.guide-progress{display:inline-block}.case-picker{grid-template-columns:1fr 1fr}.step-panel{padding:22px 15px}.step-actions{flex-wrap:wrap}.step-actions .primary{margin-left:0}.method-grid,.model-modules{grid-template-columns:1fr}}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Hojas de ruta para resolver problemas de diseño de software paso a paso">
|
||||
<title>Guías paso a paso · SAP-TFI-2026</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="guias.css">
|
||||
</head>
|
||||
<body class="guide-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 class="active" href="guias.html">Guías</a><a href="checklist.html">Checklist</a></nav>
|
||||
</header>
|
||||
|
||||
<main class="guide-shell">
|
||||
<section class="guide-hero">
|
||||
<div><p class="eyebrow">Práctica acompañada</p><h1>Resolver diseño, paso a paso</h1><p>No mires primero la respuesta. Registrá tus decisiones, pedí una pista cuando la necesites y compará tu propuesta recién al final.</p></div>
|
||||
<div class="guide-progress"><strong id="guide-completed">0 / 6</strong><span>casos completados</span></div>
|
||||
</section>
|
||||
|
||||
<section class="case-picker" id="case-picker" aria-label="Casos disponibles"></section>
|
||||
|
||||
<section class="case-workspace">
|
||||
<aside class="roadmap"><p class="eyebrow">Hoja de ruta</p><ol id="roadmap"></ol></aside>
|
||||
<article class="step-panel">
|
||||
<header><div><p class="eyebrow" id="case-label"></p><h2 id="case-title"></h2></div><span id="step-counter"></span></header>
|
||||
<div class="problem-box" id="problem-box"></div>
|
||||
<div id="step-content"></div>
|
||||
<div class="step-actions"><button id="previous-step">← Paso anterior</button><button id="hint-button">Ver una pista</button><button id="next-step" class="primary">Guardar y continuar →</button></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="method-card">
|
||||
<p class="eyebrow">Método reutilizable para la práctica completa</p>
|
||||
<h2>La misma hoja de ruta sirve para los problemas no resueltos</h2>
|
||||
<div class="method-grid"><span><b>1</b>Separar hechos de decisiones</span><span><b>2</b>Anticipar cambios</span><span><b>3</b>Un secreto por módulo</span><span><b>4</b>Diseñar interfaces</span><span><b>5</b>Elegir relaciones</span><span><b>6</b>Documentar y someter a cambios</span></div>
|
||||
<p>Aplicala a Celda de producción, Museo, Balanza de camiones, Procesamiento de imágenes, Estación climatológica, Exclusa y los demás enunciados de la práctica.</p>
|
||||
</section>
|
||||
|
||||
<footer class="guide-sources"><b>Material de estudio:</b> <a href="https://www.fceia.unr.edu.ar/ingsoft/p-diseno.pdf" target="_blank" rel="noreferrer">Práctica de Diseño ↗</a><a href="https://www.fceia.unr.edu.ar/ingsoft/problemasDiseno.pdf" target="_blank" rel="noreferrer">Problemas resueltos ↗</a></footer>
|
||||
</main>
|
||||
<dialog id="solution-dialog"><div class="solution-mark">✓</div><h2>Compará con la solución modelo</h2><div id="solution-content"></div><button id="close-solution">Cerrar</button></dialog>
|
||||
<script src="guias.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
const cases=[
|
||||
{id:"limite",short:"Límite",title:"Controlar extracciones",problem:"Una caja de ahorros ya permite depositar, extraer y consultar saldo. Se agrega la regla: no extraer más de $10.000.",steps:[
|
||||
["Separá lo estable de lo nuevo","¿Qué responsabilidad ya existe y cuál aparece con el nuevo requisito?","No confundas operaciones básicas de la cuenta con la política de límite."],
|
||||
["Detectá los ítems de cambio","¿Qué podría variar de forma independiente? Anotá al menos dos cambios futuros.","El monto puede cambiar, desaparecer o incorporar excepciones."],
|
||||
["Elegí los módulos","¿Qué secreto debería ocultar cada módulo?","Un módulo conserva la cuenta básica y otro la regla de extracción."],
|
||||
["Diseñá las interfaces","Escribí las operaciones públicas. ¿Conviene que ambos módulos presenten la misma interfaz?","Una interfaz compatible permite sustituir el objeto sin modificar clientes."],
|
||||
["Definí la relación","¿Quién usa o delega en quién? Representalo con una flecha.","El control envuelve o compone la cuenta y delega las operaciones permitidas."],
|
||||
["Someté el diseño a cambios","Probá cambiar $10.000 por un límite diario y luego eliminar la regla. ¿Qué módulos tocás?","El diseño es fuerte si CajaAhorros permanece intacta."]],solution:{idea:"Separar la política de la cuenta básica y usar delegación.",modules:[["CajaAhorros","Oculta representación y algoritmos del saldo."],["ControlExtraccion","Oculta la regla del límite y delega en CajaAhorros."]],relation:"ControlExtraccion ◇──▷ CajaAhorros; interfaces compatibles."}},
|
||||
{id:"form",short:"Formulario",title:"Validar nombre y DNI",problem:"Un formulario recibe nombre y DNI. Al aceptar, el sistema verifica que el DNI exista y corresponda con el nombre usando una fuente de datos.",steps:[
|
||||
["Marcá las decisiones","¿Qué decisiones distintas hay en interfaz, regla y persistencia?","Pensá qué cambia si reemplazás GUI, regla o base relacional por separado."],
|
||||
["Listá cambios probables","Proponé una variación para cada decisión.","La presentación puede ser web; la regla puede pedir otro dato; la fuente puede ser una API."],
|
||||
["Asigná un secreto por módulo","Nombrá tres módulos y escribí el secreto de cada uno.","Frontera, regla de negocio y acceso a datos son tres motivos de cambio."],
|
||||
["Definí servicios abstractos","Diseñá interfaces sin SQL, widgets ni rutas de archivos.","La regla pregunta por una persona; no debería conocer SELECT ni tablas."],
|
||||
["Trazá la estructura de uso","Ordená: actor, formulario, control, repositorio.","La dependencia avanza desde la frontera hacia servicios abstractos."],
|
||||
["Revisá con ICONIX","Mapeá los módulos a Boundary, Control y Entity/Repository.","Formulario=Boundary; validación=Control; acceso a personas=servicio de datos."]],solution:{idea:"Aislar presentación, regla de validación y acceso a datos.",modules:[["FormNombreDNI","Oculta la implementación del formulario."],["ControlarNombreDNI","Oculta la regla de correspondencia."],["RepositorioPersonas","Oculta tecnología y esquema de persistencia."]],relation:"Form → Control → Repositorio; ninguna sentencia SQL en los dos primeros."}},
|
||||
{id:"cuentas",short:"Cuentas",title:"Modelar tipos de cuenta",problem:"Caja de ahorro y cuenta corriente comparten operaciones, pero la primera no permite saldo negativo y la segunda sí.",steps:[
|
||||
["Compará comportamiento","Armá una tabla: qué operaciones comparten y cuáles cambian.","depositar y saldo son equivalentes; extraer tiene reglas distintas."],
|
||||
["Evitá banderas de tipo","Explicá el costo de una clase con tipo=CA/CC y condicionales.","Cada nuevo tipo agrega ramas y mezcla secretos."],
|
||||
["Separá implementaciones","Proponé una clase para cada variación de comportamiento.","Cada cuenta puede implementar extraer sin preguntar qué tipo es."],
|
||||
["Extraé una interfaz común","¿Qué abstracción permite tratar todas las cuentas de manera uniforme?","Una CuentaBancaria abstracta declara el contrato compartido."],
|
||||
["Elegí herencia con criterio","Dibujá generalización y justificá por qué es herencia de interfaz.","La sustitución sirve para clientes como calcularSaldoTotal."],
|
||||
["Probá extensión","Agregá una cuenta remunerada. ¿Cuántas clases existentes modificás?","El resultado deseable es agregar un heredero sin modificar los anteriores."]],solution:{idea:"Polimorfismo en lugar de una bandera de tipo y condicionales.",modules:[["CuentaBancaria","Interfaz abstracta común."],["CajaAhorros","Regla de extracción con saldo suficiente."],["CuentaCorriente","Permite saldo negativo."]],relation:"CajaAhorros y CuentaCorriente heredan la interfaz CuentaBancaria."}},
|
||||
{id:"idioma",short:"Idioma",title:"Internacionalizar un formulario",problem:"El título y las etiquetas del formulario Nombre/DNI deben cambiar según el idioma configurado.",steps:[
|
||||
["Localizá los condicionales","¿Dónde aparecerían sentencias case si resolvieras todo dentro de dibujar()?","Habría ramas para título, etiquetas y botones."],
|
||||
["Separá motivos de cambio","¿Qué cambia por idioma y qué cambia por tecnología de interfaz?","Traducción y dibujo son decisiones independientes."],
|
||||
["Proponé una abstracción","¿Qué servicio puede pedir el formulario sin conocer un idioma concreto?","Puede solicitar textos mediante claves o una interfaz de recursos."],
|
||||
["Elegí implementaciones","Definí al menos dos implementaciones intercambiables.","RecursosES y RecursosEN implementan el mismo contrato."],
|
||||
["Usá composición","Mostrá cómo recibe el formulario su proveedor de textos.","El proveedor se inyecta al construir el formulario."],
|
||||
["Aplicá abierto/cerrado","Agregá portugués y contá qué cambia.","Se agrega una implementación y configuración; el formulario no cambia."]],solution:{idea:"Separar recursos lingüísticos de la implementación visual.",modules:[["RecursosFormulario","Interfaz de textos necesarios."],["RecursosES / RecursosEN","Implementaciones por idioma."],["FormNombreDNI","Dibuja usando un proveedor compuesto."]],relation:"FormNombreDNI ◇── RecursosFormulario; implementaciones sustituibles."}},
|
||||
{id:"reuso",short:"Reuso",title:"Compartir implementación sin mezclar reglas",problem:"CajaAhorros y CuentaCorriente repiten el código de depositar y consultar saldo, pero difieren al extraer.",steps:[
|
||||
["Identificá repetición","¿Qué código es realmente común y qué parte expresa una regla variable?","La representación del saldo es común; la autorización de extracción difiere."],
|
||||
["Evaluá si es un ítem de cambio","¿La implementación común podría divergir entre tipos de cuenta?","Si puede divergir, heredar código crea acoplamiento evolutivo."],
|
||||
["Compará alternativas","Contrastá copiar, heredar implementación y componer un colaborador.","La composición permite delegar sin forzar una jerarquía de implementación."],
|
||||
["Diseñá el colaborador","Definí operaciones privadas de implementación del saldo.","Un objeto CuentaImp puede mantener y modificar el saldo."],
|
||||
["Dibujá ambas relaciones","Mostrá generalización de interfaz y composición de implementación.","Son relaciones con propósitos distintos."],
|
||||
["Documentá lo que UML no muestra","Escribí cómo delega cada operación y qué secreto queda oculto.","El diagrama solo no especifica interfaces, secretos ni delegación."]],solution:{idea:"Combinar herencia de interfaz con composición para compartir implementación.",modules:[["CuentaBancaria","Contrato común."],["CajaAhorros / CuentaCorriente","Reglas específicas."],["CuentaImp","Estado y operaciones básicas del saldo."]],relation:"Las cuentas heredan CuentaBancaria y cada instancia compone su propio CuentaImp."}},
|
||||
{id:"secretos",short:"Secretos",title:"Un secreto por módulo",problem:"Un formulario localizado mezcla la forma de dibujarse con el idioma de sus textos. Debe poder cambiar cualquiera sin tocar la otra decisión.",steps:[
|
||||
["Enumerá secretos","¿Cuántas decisiones independientes oculta hoy el formulario?","Al menos dos: presentación y traducción."],
|
||||
["Refiná la granularidad","¿Conviene un módulo por título o un proveedor para todos los textos? Justificá.","La granularidad debe conservar cohesión sin crear interfaces inmanejables."],
|
||||
["Abstraé la interfaz","Escribí servicios que no revelen archivos, diccionarios ni variables de entorno.","titulo(), etiquetaNombre() o texto(clave) describen necesidades, no almacenamiento."],
|
||||
["Encapsulá implementaciones","Anotá qué detalles deben permanecer privados.","Codificación, archivo, locale y cache son internos al proveedor."],
|
||||
["Configurá en un punto estable","Decidí dónde seleccionar el idioma concreto.","La composición se realiza en el punto de arranque, no dentro del formulario."],
|
||||
["Cerrá con guía de módulos","Para cada módulo escribí Función, Secreto, Interfaz y Relaciones.","La figura UML comunica relaciones, pero no reemplaza esta documentación."]],solution:{idea:"La calidad surge de explicitar función, secreto, interfaz y delegación.",modules:[["FormNombreDNI","Oculta dibujo e interacción."],["TextosFormulario","Oculta selección y representación de textos."],["Configurador","Elige implementaciones al iniciar."]],relation:"Configurador crea Textos; Form lo recibe por composición y sólo conoce su interfaz."}}
|
||||
];
|
||||
|
||||
const stepNames=["Comprender","Anticipar cambios","Descomponer","Diseñar interfaz","Relacionar","Validar"];
|
||||
let caseIndex=0,stepIndex=0;
|
||||
let state=JSON.parse(localStorage.getItem("iconix-guides")||"{}");
|
||||
function key(){return`${cases[caseIndex].id}-${stepIndex}`}
|
||||
function save(){localStorage.setItem("iconix-guides",JSON.stringify(state))}
|
||||
function caseDone(c){return c.steps.every((_,i)=>state[`${c.id}-${i}`]?.done)}
|
||||
function renderPicker(){document.querySelector("#case-picker").innerHTML=cases.map((c,i)=>`<button class="case-button ${i===caseIndex?"active":""} ${caseDone(c)?"done":""}" data-case="${i}"><span>CASO ${i+1}${caseDone(c)?" · ✓":""}</span>${c.short}</button>`).join("");document.querySelector("#guide-completed").textContent=`${cases.filter(caseDone).length} / ${cases.length}`}
|
||||
function render(){const c=cases[caseIndex],s=c.steps[stepIndex],entry=state[key()]||{};document.querySelector("#case-label").textContent=`Caso ${caseIndex+1} · ${stepNames[stepIndex]}`;document.querySelector("#case-title").textContent=c.title;document.querySelector("#step-counter").textContent=`PASO ${stepIndex+1} DE 6`;document.querySelector("#problem-box").innerHTML=`<b>Enunciado de trabajo</b><br>${c.problem}`;document.querySelector("#roadmap").innerHTML=c.steps.map((x,i)=>`<li class="${i===stepIndex?"active":""} ${state[`${c.id}-${i}`]?.done?"done":""}" data-step="${i}"><i>${state[`${c.id}-${i}`]?.done?"✓":i+1}</i>${x[0]}</li>`).join("");document.querySelector("#step-content").innerHTML=stepIndex<5?`<div class="step-question"><h3>${s[0]}</h3><p>${s[1]}</p><textarea class="answer-area" id="answer" placeholder="Escribí tu razonamiento antes de pedir una pista…">${entry.answer||""}</textarea><div class="hint-box ${entry.hint?"visible":""}" id="hint-box"><b>Pista:</b> ${s[2]}</div><div class="checklist"><label><input type="checkbox" data-check="reason" ${entry.reason?"checked":""}> Justifiqué la decisión en función de cambios probables.</label><label><input type="checkbox" data-check="terms" ${entry.terms?"checked":""}> Usé vocabulario de diseño: secreto, interfaz, responsabilidad o relación.</label></div></div>`:`<div class="step-question"><h3>${s[0]}</h3><p>${s[1]}</p><textarea class="answer-area" id="answer" placeholder="Registrá el resultado de someter tu diseño a cambios…">${entry.answer||""}</textarea><div class="hint-box ${entry.hint?"visible":""}" id="hint-box"><b>Pista:</b> ${s[2]}</div><div class="solution-card"><h3>Antes de comparar</h3><p>Comprobá que cada módulo tenga un secreto, que las interfaces no revelen implementación y que un cambio afecte pocas unidades.</p><button id="reveal-solution">Comparar con la solución modelo</button></div></div>`;document.querySelector("#previous-step").disabled=stepIndex===0;document.querySelector("#next-step").textContent=stepIndex===5?"Completar caso ✓":"Guardar y continuar →";renderPicker()}
|
||||
function persist(){const entry=state[key()]||{};entry.answer=document.querySelector("#answer")?.value||"";document.querySelectorAll("[data-check]").forEach(x=>entry[x.dataset.check]=x.checked);state[key()]=entry;save()}
|
||||
function solutionHTML(c){return`<p>${c.solution.idea}</p><div class="model-modules">${c.solution.modules.map(m=>`<div class="model-module"><strong>${m[0]}</strong><small>${m[1]}</small></div>`).join("")}</div><p><b>Relación:</b> ${c.solution.relation}</p><p><small>Esta es una solución de referencia. Compará los criterios y no sólo los nombres.</small></p>`}
|
||||
document.querySelector("#case-picker").addEventListener("click",e=>{const b=e.target.closest("[data-case]");if(b){persist();caseIndex=Number(b.dataset.case);stepIndex=0;render()}});document.querySelector("#roadmap").addEventListener("click",e=>{const li=e.target.closest("[data-step]");if(li){persist();stepIndex=Number(li.dataset.step);render()}});document.querySelector("#previous-step").addEventListener("click",()=>{persist();if(stepIndex>0)stepIndex--;render()});document.querySelector("#next-step").addEventListener("click",()=>{persist();state[key()].done=true;save();if(stepIndex<5)stepIndex++;else{document.querySelector("#solution-content").innerHTML=solutionHTML(cases[caseIndex]);document.querySelector("#solution-dialog").showModal()}render()});document.querySelector("#hint-button").addEventListener("click",()=>{const entry=state[key()]||{};entry.hint=true;state[key()]=entry;save();document.querySelector("#hint-box")?.classList.add("visible")});document.querySelector("#step-content").addEventListener("click",e=>{if(e.target.id==="reveal-solution"){persist();document.querySelector("#solution-content").innerHTML=solutionHTML(cases[caseIndex]);document.querySelector("#solution-dialog").showModal()}});document.querySelector("#close-solution").addEventListener("click",()=>document.querySelector("#solution-dialog").close());render();
|
||||
@@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Prototipo educativo de fronteras ICONIX para SAP-TFI-2026">
|
||||
<title>Prototipo ICONIX · SAP-TFI-2026</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="#presupuesto" aria-label="Ir al inicio">
|
||||
<span class="brand-mark">S</span>
|
||||
<span><strong>SAP · TFI</strong><small>Laboratorio ICONIX</small></span>
|
||||
</a>
|
||||
<div class="course lab-links"><a class="active" href="index.html">Prototipo</a><a href="ejercicios.html">Ejercicios</a><a href="guias.html">Guías</a><a href="checklist.html">Checklist</a></div>
|
||||
</header>
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="sidebar" aria-label="Escenarios">
|
||||
<p class="eyebrow">Flujo comercial</p>
|
||||
<nav id="scenario-nav">
|
||||
<button class="nav-item active" data-scenario="presupuesto"><span>01</span>Presupuesto<small>CU-VEN-001</small></button>
|
||||
<button class="nav-item" data-scenario="confirmacion"><span>02</span>Confirmación<small>CU-VEN-004</small></button>
|
||||
<button class="nav-item" data-scenario="reserva"><span>03</span>Reserva<small>CU-ENT-002</small></button>
|
||||
<button class="nav-item" data-scenario="factura"><span>04</span>Facturación<small>CU-FAC-001</small></button>
|
||||
</nav>
|
||||
<div class="legend">
|
||||
<p class="eyebrow">Leyenda ICONIX</p>
|
||||
<span><i class="dot boundary"></i>Frontera</span>
|
||||
<span><i class="dot control"></i>Control</span>
|
||||
<span><i class="dot entity"></i>Entidad</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="stage">
|
||||
<div class="stage-heading">
|
||||
<div><p class="eyebrow" id="diagram-id">D-ROB-VEN-001</p><h1 id="screen-title">Crear presupuesto</h1></div>
|
||||
<div class="view-switch" aria-label="Vista del prototipo">
|
||||
<button class="active" data-view="ui">Pantalla</button>
|
||||
<button data-view="trace">Trazabilidad</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ui-view" class="view active">
|
||||
<div class="browser-frame">
|
||||
<div class="browser-bar"><span></span><span></span><span></span><p>Ventas / Presupuestos / Nuevo</p></div>
|
||||
<div class="app-shell">
|
||||
<div class="actionbar">
|
||||
<div id="primary-actions"></div>
|
||||
<span class="record-state" id="record-state">BORRADOR</span>
|
||||
</div>
|
||||
|
||||
<div id="screen-presupuesto" class="screen active">
|
||||
<div class="form-title"><span>Presupuesto</span><strong id="order-name">Nuevo</strong></div>
|
||||
<div class="form-grid">
|
||||
<label>Cliente<input id="customer" value="Industrias Horizonte S.A."></label>
|
||||
<label>Vendedor<input value="Ana Torres"></label>
|
||||
<label>Lista de precios<select><option>ARS · Mayorista</option></select></label>
|
||||
<label>Validez<input type="date" value="2026-09-30"></label>
|
||||
</div>
|
||||
<div class="tabs"><button class="active">Líneas del pedido</button><button>Otra información</button></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>Producto</th><th>Cant.</th><th>Precio</th><th>Dto.</th><th>Impuesto</th><th>Subtotal</th></tr></thead><tbody id="order-lines"></tbody></table></div>
|
||||
<button class="text-button" id="add-line">+ Agregar producto</button>
|
||||
<div class="totals"><span>Base imponible <b id="untaxed">$ 0</b></span><span>IVA 21% <b id="tax">$ 0</b></span><strong>Total <b id="total">$ 0</b></strong></div>
|
||||
</div>
|
||||
|
||||
<div id="screen-confirmacion" class="screen">
|
||||
<div class="summary-card"><span class="icon success">✓</span><div><p>Pedido listo para confirmar</p><h2>S00042 · Industrias Horizonte S.A.</h2></div><strong>$ 588.060</strong></div>
|
||||
<div class="validation-grid"><article><span>Estado</span><strong>Presupuesto enviado</strong></article><article><span>Líneas</span><strong>2 productos válidos</strong></article><article><span>Stock</span><strong>Se generará una entrega</strong></article><article><span>Facturación</span><strong>Según cantidades entregadas</strong></article></div>
|
||||
<div class="callout"><b>Al confirmar</b><p>El sistema cambia el estado del pedido, fija la fecha y genera el picking de almacén.</p></div>
|
||||
</div>
|
||||
|
||||
<div id="screen-reserva" class="screen">
|
||||
<div class="form-title"><span>Transferencia</span><strong>WH/OUT/00031</strong></div>
|
||||
<div class="progress"><span class="done">Pedido</span><span class="active">Reserva</span><span>Preparado</span><span>Entregado</span></div>
|
||||
<div class="table-wrap"><table><thead><tr><th>Producto</th><th>Demanda</th><th>Disponible</th><th>Reservado</th><th>Resultado</th></tr></thead><tbody><tr><td>Notebook Pro 14</td><td>2</td><td>5</td><td id="reserved-a">0</td><td><span class="pill waiting" id="reserve-state-a">Pendiente</span></td></tr><tr><td>Dock USB-C</td><td>3</td><td>1</td><td id="reserved-b">0</td><td><span class="pill waiting" id="reserve-state-b">Pendiente</span></td></tr></tbody></table></div>
|
||||
<div class="callout warning"><b>Disponibilidad parcial</b><p>La segunda línea puede requerir reabastecimiento según la regla configurada.</p></div>
|
||||
</div>
|
||||
|
||||
<div id="screen-factura" class="screen">
|
||||
<div class="summary-card"><span class="icon invoice">$</span><div><p>Pedido confirmado</p><h2>S00042 · Pendiente de facturar</h2></div><strong>$ 588.060</strong></div>
|
||||
<div class="invoice-options"><label><input type="radio" name="invoice" checked> Factura regular <small>Facturar las líneas disponibles.</small></label><label><input type="radio" name="invoice"> Anticipo porcentual <small>Crear una factura por un porcentaje.</small></label></div>
|
||||
<div class="model-preview"><p class="eyebrow">Datos que se crearán</p><div><span>account.move</span><b>Estado: draft</b></div><div><span>account.move.line</span><b>2 líneas</b></div><div><span>account.tax</span><b>IVA 21%</b></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-log"><div><p class="eyebrow">Acción observada</p><strong id="event-title">Formulario preparado</strong></div><p id="event-detail">Seleccioná una acción para ver su relación con ICONIX.</p></div>
|
||||
</div>
|
||||
|
||||
<div id="trace-view" class="view">
|
||||
<div class="trace-intro"><p>Esta vista une cada elemento visible con el control que invoca y las entidades que consulta o modifica.</p><a id="diagram-link" href="../diagrams/svg/D-ROB-VEN-001%20%E2%80%94%20Diagrama%20de%20Robustez:%20Crear%20Presupuesto.svg" target="_blank">Abrir diagrama SVG ↗</a></div>
|
||||
<div class="trace-grid" id="trace-grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
# Entregas finales de alumnos
|
||||
|
||||
Cada alumno debe entregar su Trabajo Final dentro de una carpeta propia con
|
||||
este formato:
|
||||
|
||||
```text
|
||||
trabajo_final/
|
||||
└── Apellido_Nombre/
|
||||
├── README.md
|
||||
└── ...archivos del trabajo...
|
||||
```
|
||||
|
||||
## Procedimiento obligatorio
|
||||
|
||||
1. Hacer un **fork** de
|
||||
[cursos-uai/sap_tfi_2026](https://github.com/cursos-uai/sap_tfi_2026/fork)
|
||||
en la cuenta personal de GitHub.
|
||||
2. Crear una rama en el fork para la entrega.
|
||||
3. Crear `trabajo_final/Apellido_Nombre/`, sin espacios, y colocar allí todos
|
||||
los archivos. No modificar carpetas de otros alumnos.
|
||||
4. Agregar dentro de la carpeta personal un `README.md` con nombre, comisión,
|
||||
link al documento borrador o final y una breve descripción de la versión.
|
||||
5. Hacer commit y push de la rama al fork.
|
||||
6. Abrir un **pull request** hacia la rama `main` de este repositorio.
|
||||
|
||||
## Actualizaciones
|
||||
|
||||
Cada actualización de una entrega se presenta mediante un **nuevo pull
|
||||
request**. El título recomendado es:
|
||||
|
||||
```text
|
||||
Entrega TP — Apellido Nombre — versión N
|
||||
```
|
||||
|
||||
Antes de abrirlo, sincronizar el fork con `main`, crear una nueva rama, cambiar
|
||||
únicamente la carpeta personal y describir en el PR qué se actualizó.
|
||||
|
||||
> No subir credenciales, contraseñas, tokens, datos personales sensibles,
|
||||
> bases de datos productivas ni archivos que no correspondan al TP.
|
||||
Reference in New Issue
Block a user