Antes de empezar
Los tres bloques se explican sobre el mismo sistema: una aplicación web de Google Apps Script donde el coordinador registra el plan de ciclo de su carrera y el director asigna los docentes. El servidor son los archivos .js de AppScript/; el navegador recibe una sola página armada con los módulos .html. Los datos viven en una hoja de cálculo de Google («SISCargaLectiva-BD») y la oferta sale de los .xlsx de una carpeta de Drive.
Dos convenciones del código que conviene decir al empezar cualquier bloque:
- Las funciones que terminan en
_son privadas: el navegador no puede llamarlas. Las demás (guardarPlan,abrirEdicion…) son los endpoints. - Todo endpoint responde
{ok: true, datos}o{ok: false, codigo, mensaje, detalle}. El navegador decide qué hacer por elcodigo(por ejemploBLOQUEADOoCONFLICTO), nunca por el texto.
La vista previa local: la aplicación sin Google
Para practicar sin tocar la aplicación publicada, el repositorio trae un servidor local que ejecuta el mismo código del servidor con imitaciones en memoria de Drive, Sheets, la caché y el candado (AppScript/pruebas/falsos.mjs). Al arrancar ejecuta instalar(), crea las cuentas de prueba y sube los tres Excel de ejemplo. Desde la raíz del repositorio:
node AppScript/pruebas/servidor-local.mjs
Después abre en el navegador la dirección con la cuenta que quieras usar:
http://127.0.0.1:8765/?usuario=mfloresl@ulasalle.edu.pe
?usuario= distinto. La subida de .xlsx necesita internet, porque SheetJS se descarga de su CDN.
Las capturas de esta guía se tomaron así, en Chrome a 1280 × 860. Las imágenes de las pestañas de la hoja del sistema son reconstrucciones de los datos que la vista previa tenía en memoria en ese momento: muestran las mismas filas y columnas que vería Google Sheets, pero no son una captura de Google Sheets.
Cuentas de prueba
En la vista previa, la pestaña Usuarios se llena con estas cuentas (USUARIOS_PRUEBA en falsos.mjs). En la aplicación publicada los roles los decide la pestaña Usuarios real.
| Cuenta | Rol | Qué puede hacer | Úsala para |
|---|---|---|---|
| mfloresl@ulasalle.edu.pe | Coordinador de las 8 carreras | Editar, guardar y registrar cualquier plan; consultar la asignación docente | Bloques 3 y 4; primera cuenta del bloque 2 |
| fmaresg@ulasalle.edu.pe | Coordinador de las 8 carreras | Lo mismo que mfloresl | Segunda cuenta para ver bloqueos (bloque 2) |
| jhuamanih@ulasalle.edu.pe | Director de DIMA | Asignar docentes en ING, IND, ARQ e ICO; derivar los cursos que asigna DIMA | Carga masiva de asignaciones (bloque 3); derivar (bloque 4) |
| jortizc@ulasalle.edu.pe | Director de DAHUC | Asignar docentes en PSI y COM | Ver un curso derivado a DAHUC |
| afrontadof@ulasalle.edu.pe | Director de DACJ | Asignar docentes en DER | — |
| aarenasc@ulasalle.edu.pe | Director de DACTE | Asignar docentes en ADM | — |
Cómo encajan los tres bloques
Camino de un dato, del Excel a una versión guardada
interpretar()Dominio.fusionar()guardarPlan()registrarVersion_()El bloque 3 convierte tablas en asignaturas y grupos. El bloque 4 es la pantalla donde el coordinador trabaja con ellos. El bloque 2 está debajo de ambos: decide cómo se guarda cada cambio, quién puede escribir en cada momento y cómo no se pierde nada. Los tres usan Dominio.js, la lógica pura que se ejecuta igual en el servidor y en el navegador.
Bloque 2
Persistencia y concurrencia
Responsable: jhuamanih@ulasalle.edu.pe
2.1Ficha del bloque
- Pregunta que responde
- ¿Cómo se guarda cada cambio sin perder nada y sin que dos personas se pisen, usando una hoja de cálculo como base de datos?
- Archivos del servidor
BaseDatos.js 328Versionado.js 305Bloqueos.js 142Recuperacion.js 122Migraciones.js 90- Archivos del navegador
Edicion.html 186Pestanas.html 161- Lo usa
- Planes.js y Fusiones.js (bloque 4), FuenteDrive.js (bloque 3), CargaDocente.js (bloque 5), GrillaEdicion.html y AsignacionEdicion.html, que crean su sesión de edición con
crearSesionEdicion. - Depende de
Respuesta.js(fallo_y los códigos),Config.js(minutosBloqueo),Dominio.REFyDominio.isoPeru.- Pruebas
AppScript/pruebas/infraestructura.test.mjs(versionado común, bloqueos con referencia, escrituras y recuperación) y, enservidor.test.mjs, las pruebas del bloqueo por carrera, la caducidad y el latido, el conflicto, la restauración y el plan repartido en varias celdas.- Idea central
- Nada se sobrescribe: cada guardado añade una versión. Dos mecanismos distintos evitan los choques: el candado del script (milisegundos) y el bloqueo de edición (minutos).
2.2Guion de exposición · unos 12 minutos
-
0:00 – 1:00
El problema
«Varias personas editan los mismos planes desde su navegador y no tenemos un servidor de base de datos: tenemos una hoja de cálculo de Google. Este bloque resuelve tres riesgos: que un guardado borre otro, que dos personas editen lo mismo a la vez y que se pierda lo que no se guardó si se cierra la pestaña.»
Muestra: el tablero con la columna «Editando» (captura 2.6).
-
1:00 – 3:00
La hoja como base de datos
«
BaseDatos.jsdefine ocho pestañas enTABLAS_, cada una con sus columnas. Todo se guarda como texto plano, con formato@, porque Sheets convertiría “2026-1” en una fecha. Un texto que empieza por igual, más, menos o arroba se escribe con un apóstrofo para que nunca se ejecute como fórmula. Se lee conleerTabla_, que devuelve cada fila como un objeto y recuerda su número de fila, y se escribe en bloque conescribirFilas_, en una sola llamada.»Muestra: la pestaña Planes (captura 2.1) y
BaseDatos.js:4(TABLAS_). -
3:00 – 5:30
Versiones: índice más historial
«Cada documento, sea un plan o una carga docente, usa dos pestañas. El índice tiene una fila por documento con su versión vigente y la fila exacta donde está guardada. La tabla de versiones tiene una fila por guardado con el contenido completo en JSON.
registrarVersion_añade la versión y actualiza el índice. Como el contenido de una versión nunca cambia, se guarda seis horas en la caché. Restaurar no borra nada: crea una versión nueva con el contenido viejo.»Muestra: el historial (captura 2.9), la v3 creada al restaurar (2.10) y la pestaña Versiones (2.11).
-
5:30 – 8:00
El bloqueo de edición
«Cuando alguien pulsa Editar, el servidor anota en la pestaña Bloqueos que ese plan es suyo hasta una hora de caducidad: treinta minutos después de la última actividad. Mientras haga clic o escriba, el navegador manda un latido, como mucho una vez por minuto, que mueve esa hora. Otra cuenta ve el plan bloqueado y solo puede consultarlo. Si nadie hace nada en treinta minutos, el navegador guarda lo pendiente como versión y suelta el plan.»
Muestra: dos pestañas con cuentas distintas (capturas 2.4 a 2.8) y la expiración (2.21).
-
8:00 – 9:30
Conflicto de versiones: advertir, no prohibir
«El bloqueo no cubre todos los casos: la misma persona puede tener el plan abierto en dos pestañas, o el Excel puede actualizarse mientras se edita. Por eso cada guardado envía la versión sobre la que se trabajó. Si en el servidor ya hay una más nueva, responde
CONFLICTOy el navegador pregunta. Si la persona insiste, se guarda conforzar, y la otra versión sigue en el historial.»Muestra: capturas 2.14 a 2.16.
-
9:30 – 10:45
Copia de recuperación
«Cada minuto, si hay cambios sin guardar, el navegador manda una copia a la pestaña Recuperacion. No es una versión: es una sola fila por persona y documento. Si la página se cae, al volver a editar se ofrece recuperarla. Se escribe con un candado breve de dos segundos: si el sistema está ocupado con un guardado de verdad, la copia se salta y se repite al minuto siguiente.»
Muestra: la pestaña Recuperacion (2.17) y el diálogo (2.18).
-
10:45 – 11:30
Avisos entre pestañas
«Las pestañas del mismo navegador se avisan con
BroadcastChannelcuando alguien toma o suelta un plan, guarda una versión o relee los Excel. Así el tablero se actualiza sin pedirlo al servidor. Es solo una comodidad: la fuente de verdad sigue siendo el servidor, y sin el canal todo funciona igual.»Muestra: captura 2.13 o 2.15.
-
11:30 – 12:00
Migraciones y cierre
«
instalar()se puede repetir sin riesgo: crea las pestañas que falten y aplica dos migraciones que solo tocan lo que aún está en el formato viejo: las fechas pasan a hora de Perú y las cargas por departamento pasan a cargas por carrera. Resumen: versiones que nunca se borran, un candado para las escrituras, un bloqueo para las personas y una copia por si algo falla.»
2.3Conceptos clave
Las ocho pestañas de «SISCargaLectiva-BD»
| Pestaña | Columnas | Para qué sirve | Quién escribe |
|---|---|---|---|
| Usuarios | correo, rol, carreras, departamento | Rol de cada cuenta (se guarda 5 min en caché) | instalar() la siembra; luego se edita a mano |
| Planes | ciclo, carrera, estado, version, actualizado_por, actualizado_en, firma_xlsx, totales, fila_version | Índice de planes: versión vigente de cada carrera en cada ciclo | registrarVersion_ (REPO_PLAN_) |
| Versiones | ciclo, carrera, n, estado, tipo, usuario, fecha, motivo, contenido… | Una fila por guardado de un plan, con el JSON completo | registrarVersion_ |
| Bloqueos | ciclo, ref, correo, desde, expira | Quién edita qué documento y hasta cuándo | tomarBloqueo_, soltarBloqueo_ |
| Fuentes | tipo, archivo_id, nombre, modificado, procesado_en, avisos | Firma de los .xlsx ya procesados (bloque 3) | revisarFuentes_ |
| Cargas | ciclo, carrera, version, actualizado_por, actualizado_en, fila_version | Índice de cargas docentes | registrarVersion_ (REPO_CARGA_) |
| VersionesCarga | ciclo, carrera, n, tipo, usuario, fecha, motivo, planes, contenido… | Una fila por guardado de una carga; «planes» guarda la versión de cada plan que se tenía a la vista | registrarVersion_ |
| Recuperacion | ciclo, ref, correo, fecha, version_base, contenido… | Lo que se editaba sin guardar | guardarRecuperacion |
fila_version, dice en qué fila de Versiones está el contenido vigente.Reglas de escritura en la hoja
| Regla | Dónde | Por qué |
|---|---|---|
Todas las celdas con formato texto (@) | prepararTabla_ · BaseDatos.js:73 asegurarTamano_ · :136 | Sheets convertiría «2026-1» o «1-2» en fechas y quitaría ceros. Las filas y columnas nuevas se crean ya con ese formato. |
Apóstrofo ante = + - @ | celdaTexto_ · :122 | Evita que un texto escrito por alguien se ejecute como fórmula (inyección de fórmulas). |
Leer devuelve objetos con _fila | leerTabla_ · :103 | Se trabaja por nombre de columna y se sabe qué fila reescribir o borrar. |
| Escribir en bloque | escribirFilas_ · :167 | Una sola llamada a setValues: cada llamada a Sheets cuesta tiempo y cuota. |
| Borrar de abajo arriba, agrupando filas seguidas | borrarFilasDonde_ · :200 | Al borrar de abajo arriba los números de las filas pendientes no cambian. |
| Contenido en trozos de 45 000 caracteres | trocear_ · :241 unirDesde_ · :232 | Una celda admite 50 000. Un plan grande ocupa varias celdas seguidas desde «contenido». |
| Claves de caché con el id de la hoja | claveCache_ · :262 | «sis:<bdId>:<tipo>:<resto>»: si se reinstala, la caché vieja no sirve datos de la hoja anterior. |
Dos niveles de exclusión
| Candado del script | Bloqueo de edición | |
|---|---|---|
| Herramienta | LockService.getScriptLock() en conCandado_ | Una fila en la pestaña Bloqueos (tomarBloqueo_) |
| Duración | Lo que tarda una escritura (milisegundos); espera hasta 30 s | 30 minutos desde la última actividad (SIS_MINUTOS_BLOQUEO) |
| Protege | Que dos ejecuciones escriban la hoja a la vez | Que dos personas editen el mismo plan o la misma carga |
| Alcance | Todo el script: una escritura a la vez | Un documento: plan:ING o carga:ING en un ciclo |
| Si está ocupado | Error OCUPADO: «Inténtalo de nuevo en unos segundos» | Error BLOQUEADO: se ofrece consultar en solo lectura |
| Quién lo libera | finally: siempre, después de SpreadsheetApp.flush() | Salir, registrar, cerrar la pestaña, expirar; si no, caduca solo |
Índice y versiones: cómo se lee la vigente sin recorrer la tabla
REPO_PLAN_ = {indice: "Planes", versiones: "Versiones", clave: "carrera"}
| ciclo | carrera | estado | version | fila_version |
|---|---|---|---|---|
| 2026-1 | DER | Registrado | 1 | 3 |
| 2026-1 | ING | Registrado | 3 | 12 → |
| 2026-1 | IND | Registrado | 3 | 13 |
| fila | carrera | n | tipo | contenido |
|---|---|---|---|---|
| 4 | ING | 1 | importación desde xlsx | [{"sem":1,… |
| 10 | ING | 2 | guardar cambios | [{"sem":1,… |
| 12 | ING | 3 | restauración de v1 | [{"sem":1,… |
Los números de fila son ilustrativos. contenidoVersion_ mira primero la caché; si no está, lee solo la fila que indica fila_version y comprueba con esLaVersion_ que sea esa versión. Si no coincide (alguien borró filas a mano), busca la fila en la lista de versiones y, si hace falta, vuelve a leer la lista sin caché.
Ciclo de vida de un bloqueo
abrirEdicion → tomarBloqueo_expira = ahora + 30 minrenovarBloqueoliberarBloqueobloqueoVigente_ la ignoratomarBloqueo_ reescribe la fila con el nuevo correoBLOQUEO_PERDIDOLínea de tiempo de una sesión de edición
renovarBloqueo mueve «expira».guardarRecuperacion.El intervalo del latido es min(60 s, minutosBloqueo × 15 s): con 30 minutos es un minuto; con 1 minuto de bloqueo serían 15 segundos.
Qué se guarda en la caché del script y cuánto
| Dato | Tiempo | Por qué no queda viejo |
|---|---|---|
| Contenido de una versión | 6 h (21 600 s, el máximo) | Una versión nunca cambia. Si pasa de 45 000 caracteres no se guarda. |
| Lista de versiones de un documento | 6 h | Se completa al guardar; si no llega a la vigente del índice, se vuelve a leer. |
| Rol de cada cuenta | 5 min | Un cambio en Usuarios tarda como mucho eso. |
| Revisión de los .xlsx | SIS_MINUTOS_REVISION_XLSX (5 min) | «Releer Excel» la fuerza. |
| Catálogo de docentes | 1 h | Se precalienta cuando le quedan menos de 10 min. |
| Docentes por grupo | 6 h | La clave lleva la versión de cada plan y de cada carga: cualquier guardado cambia la clave. |
2.4Recorrido con capturas
Usa dos pestañas: una con ?usuario=mfloresl@ulasalle.edu.pe (cuenta A) y otra con ?usuario=fmaresg@ulasalle.edu.pe (cuenta B).
-
Tomar el bloqueo de un plan
con la cuenta A, en el tablero, pulsa Editar en Ingeniería de Software.
Captura 2.2El reloj de la sesión. Antes de que el servidor responda dice «Tomando el bloqueo de edición…» y los botones de guardar están desactivados. - Verás
- La grilla se abre al instante con el contenido que trajo el tablero. En cuanto llega la respuesta, el reloj empieza a contar 30:00 hacia atrás.
- En el código
abrirEdicion(GrillaEdicion.html:107) pinta la grilla y llama de fondo aabrirEdiciondel servidor (Planes.js:200), que dentro deconCandado_ejecutatomarBloqueo_y devuelveexpiray la copia de recuperación si existe.confirmarEdicionarranca la sesión consesionPlan.iniciar(expira).- Detalle fino
- Si el navegador ya tenía la versión vigente, el servidor no vuelve a mandar el contenido (
versionConocida). Si entretanto se guardó otra versión y aún no hay cambios, se carga esa y aparece «Se cargó la versión más reciente del plan».
-
La reserva en la pestaña Bloqueos
mira la pestaña Bloqueos mientras la cuenta A edita.
Captura 2.3La fila plan:ING: ciclo, referencia, correo, desde y expira, en hora de Perú (-05:00).- En el código
tomarBloqueo_(Bloqueos.js:71) reescribe la fila si ya existía para esa referencia, o añade una. Si el bloqueo vigente es de otra cuenta devuelveok: false. «desde» se conserva en las renovaciones; «expira» siempre se mueve.
-
La otra cuenta ve el plan bloqueado
con la cuenta B, abre Registrar plan de ciclo.
Captura 2.4Para la cuenta B, ING aparece «Bloqueado», con el correo de quien edita y la versión v2 que A acaba de guardar. La barra de avance cuenta «1 en edición por otra cuenta». - En el código
leerTablero_(Planes.js:122) añade a cada fila su bloqueo vigente.pintarTablero(Tablero.html:187) muestra «Bloqueado» solo si el bloqueo es de otra cuenta.
-
Intentar editar un plan bloqueado
con la cuenta B, pulsa Editar en ING.
Captura 2.5El servidor respondió BLOQUEADOcon el correo y la hora endetalle.Captura 2.6La consulta en solo lectura dice quién lo está editando. - En el código
- Como el tablero ya sabe que otra cuenta lo tiene,
abrirEdicionno abre la grilla: llama aabrirEdicionEsperando, que espera la respuesta.falloBloqueado_(Bloqueos.js:60) arma el error yavisarBloqueado(GrillaEdicion.html:197) ofrece consultar.
-
Guardar crea una versión
con la cuenta A, haz un cambio y pulsa Guardar cambios; luego abre Historial de versiones.
Captura 2.7El historial: v1 «importación desde xlsx» escrita por el Excel y v2 «guardar cambios» con el motivo. El selector de abajo elige qué versión restaurar. - En el código
obtenerHistorial(Planes.js:266) devuelvemetaVersiones_sin el contenido ni la fila.dialogoHistorial(Edicion.html:166) es común al plan y a la carga.
-
Restaurar no borra el historial
elige v1 y pulsa Restaurar.
Captura 2.8La nota de estado ahora dice v3: restaurar la v1 creó una versión nueva de tipo «restauración de v1». Captura 2.9En Versiones quedan las tres versiones de ING y las de IND: la v2 de ING llevaba una fusión con IND, así que IND recibió su propia versión de tipo «fusión», y al restaurar la v1 la fusión se deshizo también en IND. - En el código
restaurarVersion(Planes.js:281) lee la versión pedida, la pasa porprepararFusiones_y la registra conDominio.TIPO_VERSION.restauracion(k). El estado nunca baja de Registrado (resolverEstado_).
-
Salir de la edición suelta el plan
pulsa Salir de la edición y confirma.
Captura 2.10Lo que no se guardó se descarta y se libera el bloqueo. Captura 2.11En la pestaña de la cuenta B, que consultaba ING, aparecen dos avisos: la consulta ya muestra la v3 que A restauró, sin pedirla al servidor, y el plan quedó libre. - En el código
terminarEdicion(GrillaEdicion.html:67) descarta la copia de recuperación, avisa a las otras pestañas conanunciar("soltado")y llama aliberarBloqueo. Si se cierra la pestaña, el eventopagehidellama aliberarBloqueosin esperar respuesta.
-
La misma cuenta en dos pestañas
con la cuenta A editando Derecho, abre otra pestaña con la misma cuenta y pulsa Editar en Derecho.
Captura 2.12El servidor no lo impediría, porque el bloqueo es de la misma cuenta. El navegador lo sabe por el canal entre pestañas y advierte. - En el código
otraPestanaEditandoyavisarOtraPestana(Pestanas.html:69 y :75). La lista de planes en edición se llena con los mensajes «editando», y una pestaña nueva la pide con «hola».
-
Conflicto de versiones
elige «Editar aquí de todos modos», guarda en la segunda pestaña y después guarda en la primera.
Captura 2.13Al guardar en la segunda pestaña, la primera recibe el mensaje «guardado» y avisa de que se le preguntará antes de guardar. Captura 2.14El servidor respondió CONFLICTOcon los datos de la versión v2 endetalle. «Guardar de todos modos» repite la llamada conforzar: truey crea la v3.- En el código
- El navegador manda
versionBase.conflictoPlan_(Planes.js:109) compara con la vigente;guardar(GrillaEdicion.html:223) captura el código y llama aconfirmarConflicto(Edicion.html:150).
-
Copia de recuperación
edita un plan sin guardar y cierra la pestaña de golpe (o espera a que pase un minuto). Vuelve a pulsar Editar en ese plan.
Captura 2.15La pestaña Recuperacion: una fila por cuenta y documento, con la versión sobre la que se trabajaba ( version_base) y el contenido.Captura 2.16Al volver a editar se ofrece recuperar o descartar. Si se cierra el diálogo sin elegir, la copia se conserva. Captura 2.17Con «Recuperarlos», la grilla queda con los cambios y marcada como sin guardar. - En el código
- La sesión llama a
copia()cada 60 s y solo envía si el contenido cambió desde la última copia que el servidor confirmó.guardarRecuperacion(Recuperacion.js:88) usaconCandadoSiLibre_con 2 s: si está ocupado responde{omitida: true}y se repite al minuto.abrirEdiciondevuelve la copia conleerRecuperacion_. - Para la demo
- En la captura, la copia se creó llamando a
guardarRecuperaciondesde la vista previa, que es lo mismo que hace el navegador cada minuto.
-
Bloqueo perdido
deja pasar el tiempo de bloqueo sin actividad mientras otra cuenta toma el plan; luego haz clic en la grilla.
Captura 2.18El latido recibió BLOQUEO_PERDIDO: la sesión se detiene, se vuelve al tablero y lo no guardado queda en la copia de recuperación.- En el código
renovarBloqueo_(Bloqueos.js:89) falla sitomarBloqueo_devuelveok: false. En el navegador,latido(Edicion.html:61) reconoce el código y llama aop.alPerder, que en el plan esbloqueoPerdido(GrillaEdicion.html:24).- Para la demo
- Para no esperar, en la captura se cambió el correo de la fila de Bloqueos por el de la cuenta B, que es lo que hace
tomarBloqueo_cuando otra cuenta toma un bloqueo caducado.
-
La sesión expira y se autoguarda
haz un cambio y no toques nada hasta que el reloj llegue a cero.
Captura 2.19Los cambios se guardaron como una versión «autoguardado» y se liberó el bloqueo. Para la captura se usó SIS_MINUTOS_BLOQUEO = 1.- En el código
expirar(GrillaEdicion.html:35) llama aguardarPlancontipo: "autoguardado"yforzar: true, y luego aliberarBloqueo. Si no había cambios solo suelta el plan.
2.5Catálogo de acciones y posibilidades
| Acción | Cómo se dispara | Endpoint o función | Resultado |
|---|---|---|---|
| Tomar el bloqueo de un plan | «Editar» en el tablero, «Editar este plan» en la consulta o «Seguir editando el plan» tras registrar | abrirEdicion → tomarBloqueo_ | Sesión de 30 min; BLOQUEADO si otra cuenta lo tiene |
| Renovar (latido) | Automático con clic o tecla, como mucho una vez por minuto | renovarBloqueo | Nueva «expira»; BLOQUEO_PERDIDO si otra cuenta lo tomó |
| Soltar el bloqueo | «Salir de la edición», registrar el plan, expirar o cerrar la pestaña | liberarBloqueo | Borra la fila de Bloqueos de esa cuenta |
| Guardar una versión | Guardar borrador, Registrar, Guardar cambios, autoguardado, fusión, cambio de departamento, importación | guardarPlan → registrarVersion_ | Fila nueva en Versiones; índice actualizado; copia borrada |
| Ver el historial | «Historial de versiones» en la grilla | obtenerHistorial | Lista de versiones con tipo, usuario, fecha y motivo |
| Restaurar una versión | «Restaurar» en el historial | restaurarVersion | Versión nueva «restauración de vN» |
| Guardar a pesar del conflicto | «Guardar de todos modos» | guardarPlan con forzar | Versión nueva encima de la más reciente |
| Copia de recuperación | Automática cada 60 s si hay cambios y el bloqueo está confirmado | guardarRecuperacion | Una fila por cuenta y documento; omitida si está ocupado |
| Recuperar o descartar | Diálogo al volver a editar | descartarRecuperacion | Recuperar pinta la copia; descartar la borra |
| Editar en otra pestaña de todos modos | Diálogo «Ya editas este plan en otra pestaña» | abrirEdicion(sig, true) | Dos pestañas con el mismo bloqueo; el conflicto avisará al guardar |
| Lo mismo con la carga docente | «Editar asignaciones» en Asignar docentes | abrirEdicionCarga, renovarBloqueoCarga, liberarBloqueoCarga, guardarCarga, restaurarCarga | Mismas piezas con la referencia carga:ING y el repositorio REPO_CARGA_ |
| Instalar o reinstalar | Ejecutar instalar() desde el editor de Apps Script | prepararTablas_, migraciones | Crea lo que falte; retira la pestaña obsoleta Asignaciones |
2.6Código clave comentado
Escribir texto seguro y escribir en bloque
function celdaTexto_(v) { const t = v == null ? "" : String(v); return /^[=+\-@]/.test(t) ? "'" + t : t; // nunca una fórmula } function escribirFilas_(nombre, fila, filas) { if (!filas.length) return; const h = hojaBd_(nombre); const ancho = filas.reduce(function (n, f) { return Math.max(n, f.length); }, 0); asegurarTamano_(h, fila + filas.length - 1, ancho); // filas y columnas nuevas, en formato texto h.getRange(fila, 1, filas.length, ancho).setValues(filas.map(function (f) { const v = f.map(celdaTexto_); while (v.length < ancho) v.push(""); return v; })); // una sola llamada a Sheets }
El candado del script
function conCandado_(fn) { const candado = LockService.getScriptLock(); if (!candado.tryLock(30000)) fallo_("OCUPADO", "El sistema está ocupado con otra operación. Inténtalo de nuevo en unos segundos."); try { return fn(); } finally { SpreadsheetApp.flush(); // escribe todo antes de soltar candado.releaseLock(); } }
Todo endpoint que escribe hace sus comprobaciones dentro de conCandado_: así, entre leer el bloqueo o la versión vigente y escribir la nueva, ninguna otra ejecución puede colarse. El flush antes de soltar garantiza que la siguiente ejecución ya lea lo escrito.
Registrar una versión
function registrarVersion_(repo, ciclo, clave, fila, datos) { const n = vigente_(fila).version + 1; // la siguiente versión const texto = JSON.stringify(datos.contenido); const comunes = { ciclo: ciclo, n: n, usuario: datos.usuario, fecha: datos.fecha }; comunes[repo.clave] = clave; const meta = Object.assign({}, datos.meta || {}, comunes); const camposVersion = TABLAS_[repo.versiones].slice(0, columnaContenido_(repo)); const filaVersion = agregarFila_(repo.versiones, // 1. fila nueva en Versiones camposVersion.map(function (c) { return meta[c] == null ? "" : meta[c]; }).concat(trocear_(texto))); const indice = Object.assign({}, fila || {}, datos.indice || {}, { ciclo: ciclo, version: n, actualizado_por: datos.usuario, actualizado_en: datos.fecha, fila_version: filaVersion }); indice[repo.clave] = clave; const valores = TABLAS_[repo.indice].map(function (c) { return indice[c] == null ? "" : indice[c]; }); if (fila) escribirFila_(repo.indice, fila._fila, valores); // 2. índice: reescribe else agregarFila_(repo.indice, valores); // o añade si es el primer guardado cachearTexto_(claveContenido_(repo, ciclo, clave, n), texto); // 3. caché del contenido /* …y completa en la caché la lista de versiones, si estaba */ return n; }
La misma función sirve para planes (REPO_PLAN_) y para cargas (REPO_CARGA_): solo cambian los nombres de las pestañas. Por eso registrarVersionPlan_ (Planes.js:82) y registrarVersionCarga_ son envoltorios de tres líneas. Debe llamarse dentro de conCandado_.
Tomar o renovar un bloqueo
function tomarBloqueo_(ciclo, ref, correo) { const b = leerBloqueos_().filter(function (x) { return x.ciclo === ciclo && x.ref === ref; })[0]; if (bloqueoVigente_(b) && b.correo !== correo) return { ok: false, bloqueo: b }; // es de otra cuenta const desde = bloqueoVigente_(b) ? b.desde : Dominio.isoPeru(); // se conserva al renovar const expira = Dominio.isoPeru(Date.now() + config_().minutosBloqueo * 60000); const valores = [ciclo, ref, correo, desde, expira]; if (b) escribirFila_(TABLA_.BLOQUEOS, b._fila, valores); // reutiliza la fila de esa ref else agregarFila_(TABLA_.BLOQUEOS, valores); return { ok: true, desde: desde, expira: expira }; }
El latido en el navegador
async function latido(){ if (!s.temporizador) return; // no hay sesión activa const intervalo = Math.min(60000, CONFIG.minutosBloqueo * 15000); if (Date.now() - s.ultimoLatido < intervalo) return; // como mucho uno por intervalo s.ultimoLatido = Date.now(); const generacion = s.generacion; let r; try { r = await op.renovar(); } catch (e) { if (generacion !== s.generacion || !s.temporizador) return; // respuesta de una sesión anterior if (e && e.codigo === "BLOQUEO_PERDIDO") { detener(); return op.alPerder(e.detalle ? e.detalle.correo : ""); } return; // un fallo de red: la siguiente actividad lo reintenta } if (generacion !== s.generacion || !s.temporizador) return; s.expira = Date.parse(r.expira); pintar(); if (op.alRenovar) op.alRenovar(); } ["click", "keydown"].forEach(ev => document.addEventListener(ev, latido, { passive: true }));
crearSesionEdicion es una fábrica: recibe qué hacer al renovar, al perder, al expirar y cómo copiar, y devuelve una sesión. El plan (sesionPlan, GrillaEdicion.html:9) y la carga docente (sesionCarga, AsignacionEdicion.html) son dos sesiones del mismo código. La «generación» descarta respuestas que llegan cuando ya se salió de esa sesión.
Guardar con conflicto
async function guardar(datos){ const p = app.plan, sig = app.carrera; const base = { asignaturas: p.asignaturas, versionBase: p.version, fusionesDisueltas: p.fusionesDisueltas || [] }; let r; try { r = await llamar("guardarPlan", app.ciclo, sig, Object.assign({}, base, datos)); } catch (e) { if (e.codigo !== "CONFLICTO") throw e; if (!await confirmarConflicto(e.detalle, "del plan")) return null; // advertir… r = await llamar("guardarPlan", app.ciclo, sig, Object.assign({}, base, datos, { forzar: true })); // …no prohibir } Object.assign(p, { version: r.version, estado: r.estado, sucio: false, fusionesDisueltas: [] }); sesionPlan.olvidarCopia(); sesionPlan.renovada(r.expira); anunciarGuardado(sig, r.estado, r.version, p.asignaturas); // avisa a las otras pestañas /* … */ }
La copia de recuperación con candado breve
const guardada = conCandadoSiLibre_(function () { const f = filaRecuperacion_(c, r.ref, usuario.correo); const fecha = Dominio.isoPeru(); const valores = [c, r.ref, usuario.correo, fecha, String(parseInt(String(datos && datos.versionBase), 10) || 0)].concat(trocear_(texto)); if (f) { /* limpia las celdas sobrantes y reescribe su fila */ } else agregarFila_(TABLA_.RECUPERACION, valores); return { fecha: fecha }; }, MS_CANDADO_RECUPERACION_); // 2000 ms return guardada || { fecha: null, omitida: true };
Mensajes entre pestañas
| Mensaje | Lo envía | Qué hacen las demás pestañas |
|---|---|---|
| hola | Una pestaña que arranca | Responden con «editando» si tienen un plan abierto |
| editando | Al confirmar el bloqueo y en cada renovación | Marcan el plan como bloqueado en su tablero; si es nuevo, avisan |
| soltado | Al salir, expirar, perder el bloqueo o registrar | Lo quitan del tablero; si lo consultaban, avisan de que quedó libre |
| guardado | Tras guardar, restaurar o una fusión ajena | Actualizan la fila del tablero sin pedirla al servidor; si editan ese plan, advierten |
| tablero | Tras «Releer Excel» | Vuelven a leer el tablero |
| asignaciones | Tras guardar una carga docente | Piden de nuevo los docentes por grupo y la carga, si la ven |
2.7Errores, avisos y casos límite
| Código | Cuándo ocurre | Qué ve la persona |
|---|---|---|
| OCUPADO | El candado no se consiguió en 30 s | «El sistema está ocupado con otra operación. Inténtalo de nuevo en unos segundos.» |
| BLOQUEADO | Abrir o guardar un plan que edita otra cuenta, o una fusión que toca un plan bloqueado | «El plan está siendo editado» y paso a solo lectura |
| BLOQUEO_PERDIDO | Un latido cuando el bloqueo ya es de otra cuenta | «Se perdió el bloqueo de edición» |
| CONFLICTO | Se guardó otra versión después de la versionBase | «Hay una versión más reciente del plan» con Cancelar y Guardar de todos modos |
| NO_ENCONTRADO | Restaurar una versión que no existe, o una fila de versión que desapareció | «No se encontró la versión vN del plan.» |
| NO_INSTALADO | Falta SIS_BD_ID o una pestaña sin definición | Pide ejecutar instalar() |
| DATO_INVALIDO | Referencia de recuperación que no es plan:XXX o carga:XXX | «Referencia de recuperación no válida.» |
| INTERNO | Cualquier excepción no prevista; se registra con console.error | Mensaje genérico, sin detalles técnicos |
guardarEnCache_ ignora los valores de más de 100 KB y se vuelve a leer la hoja. Si alguien borra filas a mano en Versiones, textoDeVersion_ detecta que fila_version ya no es esa versión y la busca. Un bloqueo caducado no se borra: la fila queda hasta que alguien la reutiliza o la suelta, y bloqueoVigente_ la ignora.
2.8Preguntas probables
¿Por qué una hoja de cálculo y no una base de datos?
Apps Script no trae una base de datos, la institución ya trabaja con Google Sheets y la hoja se puede auditar a mano. Sus límites se resuelven en el código: texto plano para que nada se convierta, 45 000 caracteres por celda con el contenido repartido, LockService para la concurrencia y caché para no leerla entera cada vez.
¿Qué pasa si dos personas guardan exactamente al mismo tiempo?
conCandado_ las pone en fila: la segunda espera hasta 30 s a que termine la primera. Si son el mismo plan, solo una puede tener el bloqueo; la otra recibe BLOQUEADO. Si las dos tienen el bloqueo (misma cuenta en dos pestañas), la segunda recibe CONFLICTO.
¿Cuál es la diferencia entre el candado y el bloqueo?
El candado protege la escritura física de la hoja durante milisegundos y abarca todo el script. El bloqueo protege el trabajo de una persona durante minutos y abarca un documento. Sin candado, dos guardados podrían intercalar sus escrituras; sin bloqueo, dos personas editarían el mismo plan sin saberlo.
¿Se puede perder trabajo?
Las versiones nunca se borran. Lo que aún no se guardó tiene tres redes: la copia de recuperación cada minuto, el autoguardado al expirar y el aviso del navegador si se intenta cerrar la página con cambios (beforeunload). Lo único que se pierde es lo editado en el último minuto si la página se cae.
¿Por qué el conflicto no impide guardar?
Es un principio del caso de uso: el sistema advierte, no prohíbe. La persona decide, y la otra versión sigue en el historial, así que nada es irreversible: se puede restaurar.
¿Qué pasa si cierro la pestaña sin salir de la edición?
El evento pagehide llama a liberarBloqueo sin esperar respuesta. Si esa llamada no llega, el bloqueo caduca solo a los 30 minutos. Lo no guardado queda en la copia de recuperación si ya pasó un minuto.
¿Cómo se lee la versión vigente sin recorrer toda la tabla?
El índice guarda fila_version. Se lee solo esa fila y se comprueba con esLaVersion_ que ciclo, clave y número coincidan. Además, el contenido vigente de varios planes se lee en un solo bloque cuando sus filas están cerca (contenidosVigentes_), como pasa tras leer los Excel.
¿La caché puede servir datos viejos?
No en lo importante: el contenido de una versión no cambia nunca, así que su caché es siempre correcta. Lo que sí cambia (docentes por grupo) lleva en la clave las versiones de todos los planes y cargas, y cualquier guardado cambia la clave. Las claves llevan el id de la hoja del sistema.
¿Qué pasa cuando Versiones crezca mucho?
Leer la vigente no se encarece, porque el índice dice su fila. El historial sí lee la tabla. La política propuesta (cabecera de Versionado.js, auditoría P3.6-j) es archivar en otro libro las versiones de cada ciclo cerrado y dejar solo la vigente y el último registro.
¿Las pestañas del navegador sustituyen al servidor?
No. Son avisos para que la pantalla se actualice al momento. Cada decisión (bloqueo, conflicto, permiso) la vuelve a comprobar el servidor. Si el navegador no tiene BroadcastChannel, todo funciona igual sin esos avisos.
¿Por qué las fechas terminan en -05:00?
Dominio.isoPeru guarda la hora de Perú (UTC−5, sin horario de verano) en ISO 8601 con su desfase. En la hoja se leen como hora local y Date.parse las entiende igual que las antiguas en UTC. La migración convertirFechasAPeru_ pasó las viejas a este formato.
2.9Para practicar
node --test AppScript/pruebas/infraestructura.test.mjs AppScript/pruebas/servidor.test.mjs
Bloque 3
Entrada de datos: Excel y cargas masivas
Responsable: mfloresl@ulasalle.edu.pe
3.1Ficha del bloque
- Pregunta que responde
- ¿Cómo una tabla (un Excel de Drive, un rango pegado o un archivo subido) se convierte en asignaturas, grupos y asignaciones, sin rechazar nunca lo que llega?
- Archivos del servidor
LectorXlsx.js 310OfertaXlsx.js 90FuenteDrive.js 217CargaDocenteCatalogo.js 95- Archivos del navegador
Rejilla.html 148ImportarOferta.html 184AsignacionCarga.html 191- Parte de Dominio.js
L116–240 normalización y textoL465–611 deducir columnas e interpretarL1357–1516 hoja de CargaXDepartamento- Lo usa
- El bloque 4 recibe las asignaturas interpretadas en la grilla. El bloque 2 guarda las versiones de importación. El bloque 5 recibe las asignaciones de la carga masiva y el catálogo de docentes.
- Pruebas
AppScript/pruebas/lector.test.mjs: los Excel reales dan la misma oferta que el prototipo, 8 carreras, 345 asignaturas y 365 grupos.- Idea central
- El sistema interpreta y no valida: lo que se entiende entra, lo que no se entiende se conserva como texto, y solo se toman las filas de la carrera del plan (CU-001 v4.0).
3.2Guion de exposición · unos 12 minutos
-
0:00 – 1:00
El principio: interpretar, nunca rechazar
«Hoy la oferta vive en varios Excel que cada carrera llena a su manera. Si el sistema rechazara un archivo por una columna mal escrita, nadie lo usaría. Por eso el caso de uso dice que la carga nunca se rechaza: se interpreta lo que llega, el texto que no se entiende se guarda tal cual y el coordinador lo corrige después en la grilla.»
-
1:00 – 2:00
Tres puertas de entrada
«Los datos entran por tres caminos: los Excel de la carpeta de Drive, que el sistema lee solo; la carga masiva de la oferta, donde el coordinador pega un rango o sube un archivo; y la carga masiva de asignaciones, donde el director pega la hoja de su departamento.»
Muestra: el diagrama de las tres entradas (más abajo).
-
2:00 – 4:00
Los Excel de Drive
«En la carpeta del sistema hay tres tipos de archivo y se reconocen por el nombre: CargaXCarrera, CargaXDepartamento y CargaDeDocentes. Si hay varios del mismo tipo se usa el más reciente, y si alguien lo convirtió a Hoja de Google se avisa y no se lee. El sistema nunca los modifica. Como mirar la carpeta cuesta decenas de llamadas a Drive, se mira como mucho cada cinco minutos; el botón Releer Excel lo fuerza.»
Muestra: «Releer Excel» (captura 3.2) y los avisos de la carpeta (3.4).
-
4:00 – 5:30
Saber si algo cambió
«Cada archivo tiene una firma: tipo, id de Drive y fecha de modificación, que se guarda en la pestaña Fuentes. Si la firma es igual, no se lee nada. Si cambió, se lee el Excel fuera del candado, porque tarda, y después, dentro del candado, se compara plan por plan con un hash de su contenido. Solo los planes cuyo contenido cambió reciben una versión: “importación desde xlsx” si es la primera, “actualización desde xlsx” si ya tenía.»
Muestra: la pestaña Fuentes (3.3) y
aplicarOferta_(FuenteDrive.js:181). -
5:30 – 7:00
Leer un .xlsx sin convertirlo
«Un .xlsx es un zip con archivos XML.
descomprimirXlsx_lo abre conUtilities.unzipyleerLibroXlsx_lee el libro, las cadenas compartidas, los estilos para reconocer fechas y cada hoja. Devuelve cada celda como el texto que mostraría Excel. Esta parte es una función pura, así que se prueba en Node con los Excel reales.» -
7:00 – 8:30
Deducir columnas e interpretar
«
Dominio.interpretarbusca la fila de cabeceras, con sinónimos como TH por TOTAL HORAS. Si falta alguna columna, la deduce por su contenido: un código tiene la forma 3.1.1.24, una malla es un año, un semestre puede ser “1-2”, que significa semestre 1, grupo 2. Agrupa por código, ordena por semestre e ignora las filas de otra carrera. Este mismo código corre en el servidor y en el navegador.»Muestra: pegado con cabeceras (3.7) y sin cabeceras (3.10 y 3.12).
-
8:30 – 10:00
La carga masiva de la oferta
«En la pantalla de carga hay tres opciones: pegar desde Google Sheets en una rejilla con aspecto de hoja, subir un archivo o descargar la plantilla. La rejilla muestra lo pegado y dice si reconoció las cabeceras antes de procesar. Para los libros de cálculo se usa SheetJS, que pesa casi un megabyte y por eso se descarga solo al entrar aquí. Si se reemplaza un plan, los grupos que siguen existiendo conservan su identificador y sus docentes.»
Muestra: capturas 3.5 a 3.14.
-
10:00 – 11:30
La carga masiva de asignaciones
«El director pega la hoja de su departamento de CargaXDepartamento, donde el nombre del docente aparece una vez y sus cursos debajo.
interpretarCargaDepartamentorecuerda el último docente, salta las filas de TOTAL y cruza cada fila con las secciones del ciclo por carrera, código y grupo. Cada fila queda en uno de cuatro estados, y los nombres que no están en el catálogo se resuelven con sugerencias antes de aplicar.»Muestra: capturas 3.16 a 3.21.
-
11:30 – 12:00
Cierre
«En resumen: tres entradas, una sola forma de interpretar, ninguna carga rechazada y los archivos originales intactos. Los avisos explican lo que no se pudo leer, para que se corrija en el Excel.»
3.3Conceptos clave
Las tres entradas
1 · Excel de la carpeta de Drive (servidor)
localizarXlsx_el más reciente de cada tipofirmaFuentes_fuentesIguales_descomprimirXlsx_leerLibroXlsx_ofertaDesdeLibros_aplicarOferta_versión solo si el hash del plan cambió2 · Carga masiva de la oferta (navegador)
crearRejillaPegadoleerArchivoComoMatrizmatrizDesdeTextoo SheetJS para librosDominio.interpretarcabeceras + pistasprocesarse guarda desde el bloque 43 · Carga masiva de asignaciones (navegador)
leerArchivoDocenteselige la hoja con la siglainterpretarCargaDepartamentosugerirDocentesLos archivos de la carpeta y cómo se reconocen
| Tipo | El nombre contiene | Qué aporta | Si falta |
|---|---|---|---|
| carrera | CARGAXCARRERA | La oferta: una hoja por carrera | Aviso FALTA_CARGA_CARRERA; no se carga nada |
| departamento | CARGAXDEPARTAMENTO | La CONDICIÓN de cada código: si habla de INTERNACIONAL, VIRTUAL o FUSIÓN y la asignatura no tiene observación, pasa a serlo | Se lee la oferta sin esas observaciones |
| docentes | CARGADEDOCENTES o DOCENTES | El catálogo de docentes (bloque 5) | Aviso CATALOGO_FALTA; los nombres se escriben a mano |
El nombre se normaliza (mayúsculas, sin tildes ni espacios) antes de buscar, en tipoDeFuente_ (FuenteDrive.js:94). Un archivo convertido a Hoja de Google se avisa con XLSX_CONVERTIDO y se ignora.
Columnas de la plantilla y cómo se deducen sin cabecera
| Columna | Sinónimos | Pista por contenido (puntos por celda) |
|---|---|---|
| CODIGO | — | ^\d+(\.\d+){2,3}$ → 3 (3.1.1.24) |
| MALLA | — | 19xx o 20xx → 3 |
| SEMESTRE | — | «1-2» o «1/2» → 3 · un número de 1 a 12 → 1 |
| DPTO | — | DIMA, DAHUC, DACJ, DACTE → 3 · 3 a 6 letras → 1 |
| CARRERA | — | sigla, nombre o alias de una carrera → 3 |
| ASIGNATURA | — | palabra de 4+ letras y más de 6 caracteres → 2 |
| HT, HP, TOTAL HORAS | TH y TOTAL DE HORAS = TOTAL HORAS | número de 1 o 2 cifras → 1 |
| GRUPO | — | número del 1 al 99 → 1 |
| OBSERVACIONES | OBSERVACION | cualquier letra → 1 |
deducirColumnas
Primero manda la cabecera. Para cada campo que falta, en el orden CODIGO, MALLA, CARRERA, DPTO, SEMESTRE, ASIGNATURA, HT, HP, TOTAL HORAS, GRUPO y OBSERVACIONES, puntúa cada columna libre con sus primeras 25 celdas y se queda con la mejor si su promedio pasa de 0,34. Por debajo de ese umbral no se arriesga y el campo queda vacío.
Reglas de interpretación
| Situación | Qué hace interpretar |
|---|---|
| SEMESTRE «1-2» | Semestre 1, grupo 2 (si la columna GRUPO está vacía) |
| SEMESTRE que no es número | Se conserva el texto y da nombre a su bloque en la grilla |
| Fila de otra carrera | Se ignora y se cuenta: «1 fila de otra carrera se ignoró» |
| CARRERA vacía | La fila se toma |
| Mismo código en varias filas | Una asignatura con varios grupos |
| Sin código | Se agrupa por el nombre; sin código ni nombre, cada fila vale sola |
| Mismo código y grupo repetidos | En la lectura de Drive (sinDuplicados) la segunda fila no se lee y se avisa FILA_REPETIDA; en el pegado se añade como otro grupo con el mismo número |
| HT o HP con texto | Se guarda tal cual; para sumar se toma el número que contenga |
Estados de una fila en la carga masiva de asignaciones
| Estado | Cuándo | Al aplicar |
|---|---|---|
| Se asigna | La fila coincide con una sección de la carrera que asigna el departamento de quien carga | Reemplaza las asignaciones de ese componente de la sección |
| No está en el plan | No hay sección con esa carrera, código y grupo | Se agrega «sin sección» (solo el director de la carrera) |
| De otra carrera | La fila es de una carrera distinta a la que se está cargando | Se omite |
| La asigna otro departamento | La sección existe pero la asigna otro departamento (por su carrera o por derivación) | Se omite |
3.4Recorrido con capturas
Usa ?usuario=mfloresl@ulasalle.edu.pe para la oferta y ?usuario=jhuamanih@ulasalle.edu.pe (director de DIMA) para las asignaciones.
-
Los avisos de la lectura de Drive
abre Registrar plan de ciclo en 2026-1.
Captura 3.1El Excel de ejemplo tiene en la hoja Psicología dos filas con el mismo código y grupo: la segunda no se lee y el aviso dice qué corregir en el Excel. - Verás
- Las 8 carreras en Registrado v1. La primera vez que alguien abre el tablero se leen los Excel; mientras, la página ya se muestra con lo que había.
- En el código
datosIniciales_no mira Drive para no retrasar la página: si hace falta, marcapendientey el navegador pideobtenerTablerode fondo.ofertaDesdeLibros_(OfertaXlsx.js:14) genera el avisoFILA_REPETIDA, que se guarda en Fuentes y se muestra a todos mientras el Excel no cambie.
-
Releer Excel
pulsa Releer Excel.
Captura 3.2«Excel revisado». Las otras pestañas abiertas reciben el mensaje «tablero» y vuelven a leerlo. - En el código
cargarTablero({releer: true})(Tablero.html:142) →obtenerTablero(ciclo, {releer: true})→sincronizarFuentes_(true)(FuenteDrive.js:14), que mira la carpeta aunque la caché diga que se revisó hace poco y además vuelve a leer el catálogo de docentes (calentarCatalogo_(true)).
-
La firma de los archivos
mira la pestaña Fuentes de la hoja del sistema.
Captura 3.3Una fila por archivo: tipo, id de Drive, nombre, fecha de modificación y cuándo se procesó. La fila de «carrera» guarda además los avisos de la lectura. - En el código
fuentesIguales_(FuenteDrive.js:157) compara «tipo|archivo_id|modificado» de lo guardado con lo actual. Si coinciden, no se descarga ningún archivo.
-
Problemas en la carpeta
sube a la carpeta una segunda copia de CargaXCarrera y una versión convertida a Hoja de Google; pulsa Releer Excel.
Captura 3.4Tres niveles de aviso: el archivo convertido no se lee, con dos CargaXCarrera se usa el más reciente (informativo) y el aviso de la fila repetida se mantiene. - En el código
localizarXlsx_(FuenteDrive.js:108) recorre la carpeta, descarta las Hojas de Google conXLSX_CONVERTIDO, ordena los candidatos de cada tipo por fecha y avisaVARIOS_ARCHIVOS.- Para la demo
- En la vista previa, los archivos se subieron a la carpeta simulada desde el script de capturas.
-
Un ciclo sin datos
cambia el ciclo a 2026-2.
Captura 3.5Los Excel son de 2026-1 ( SIS_CICLO_XLSX), así que en 2026-2 todo está Sin Iniciar y el plan se arma con la carga masiva. -
Las opciones de carga
pulsa Editar en Ingeniería de Software.
Captura 3.6Un plan vacío abre directamente la carga masiva: pegar, subir archivo o descargar la plantilla. Arriba, la nota del bloqueo y el reloj de la sesión (bloque 2). - En el código
entrarEnEdicionllama airACarga(ImportarOferta.html:81) cuando el plan no tiene asignaturas.irACargaempieza a descargar SheetJS por si se sube un libro.- Plantilla
- «Descargar plantilla oficial» genera en el navegador un .csv con BOM, separado por punto y coma, con las 11 columnas y una fila de ejemplo (
Plantilla_Plan_Ciclo.csv).
-
Pegar un rango con cabeceras
pulsa Pegar desde Google Sheets, copia un rango en Sheets (con su fila de cabeceras) y pulsa Ctrl+V.
Captura 3.7La rejilla vacía espera el pegado; también acepta soltar un archivo encima. Captura 3.8Con letras de columna y números de fila, como una hoja. El pie dice cuántas filas y columnas hay y en qué fila están las cabeceras. - En el código
crearRejillaPegado(Rejilla.html:92) escucha el eventopastedel documento solo mientras la rejilla está a la vista, convierte el texto conmatrizDesdeTextoy dibuja hasta 300 filas (se procesan todas).filaCabecerausaubicarCabeceraspara resaltar la fila.
-
Procesar: la fila de otra carrera se ignora
pulsa Procesar contenido.
Captura 3.9La fila de ADM se ignoró y se avisa. «Ing. Software» se reconoció como ING por su alias, «1-2» se convirtió en el grupo 2 y la observación VIRTUAL cuenta en «Intl. / virtual». - En el código
procesar(ImportarOferta.html:134) llama ainterpretar(matriz, {carrera}), conserva los identificadores de los grupos que siguen existiendo (Dominio.conservarIds), marca el plan como sucio y pinta la grilla. Todavía no se guardó nada: el guardado es del bloque 4.
-
Pegar sin cabeceras
desde la grilla, Reemplazar contenido → Continuar; pega un rango sin fila de cabeceras y con las columnas en otro orden.
Captura 3.10Volver a las opciones de carga avisa de que el contenido nuevo reemplazará todo el plan. Captura 3.11«No se reconocen las cabeceras», pero el botón Procesar sigue disponible: no se rechaza. Captura 3.12Como el plan ya tenía asignaturas, se confirma el reemplazo. Se pierden desdobles, ajustes y fusiones; los grupos con el mismo código y número conservan sus docentes. Captura 3.13Las columnas se dedujeron por su contenido: código, asignatura, semestre, HT, HP y departamento. - En el código
deducirColumnas(Dominio.js:512) con lasPISTAS(Dominio.js:469). Al reemplazar, las fusiones con otras carreras que se pierden se anotan enfusionesDisueltaspara deshacerlas también en esos planes al guardar.
-
Subir un libro con varias hojas
en otra carrera vacía, pulsa Subir archivo y elige
SISCargaLectiva-Ejemplo-CargaXCarrera.xlsx.Captura 3.14El libro tiene una hoja por carrera: se elige cuál importar. Captura 3.15La hoja «Adm. Neg. Int.» importada: 60 asignaturas y 60 grupos. - En el código
leerArchivoComoMatriz(Rejilla.html:54): .xlsx, .xlsm, .xlsb, .xls y .ods se leen con SheetJS (XLSX.read,sheet_to_jsonconraw: falsepara tener el texto que se ve). Cualquier otra extensión, o un libro que no se deja leer, se lee como texto conmatrizDesdeTexto. No hay extensiones prohibidas.
-
La pantalla de asignación del director
abre la vista previa con
?usuario=jhuamanih@ulasalle.edu.pey entra en Asignar docentes.Captura 3.16La carga de ING, que asigna DIMA. Esta pantalla es del bloque 5; aquí solo importa su botón «Carga masiva», que aparece al pulsar «Editar asignaciones». -
Pegar la hoja del departamento
Editar asignaciones → Carga masiva; pega la hoja «CARGA DIMA 2026-I» o súbela con «Subir CargaXDepartamento».
Captura 3.17La misma rejilla de la oferta, en otra pantalla. Captura 3.18El formato real: el docente aparece una vez, sus cursos debajo y una fila TOTAL entre docentes. - En el código
- Al subir el libro,
leerArchivoDocentes(AsignacionCarga.html:93) elige sola la hoja cuyo nombre contiene la sigla del departamento de quien carga; si no la encuentra, pregunta.
-
Previsualizar antes de aplicar
pulsa Previsualizar asignaciones.
Captura 3.19De la hoja de DIMA se leyeron 92 filas, con la cabecera en la fila 2: 54 se asignan, 3 no están en el plan de ING, 35 son de otra carrera (ADM, IND, ARQ…) y ninguna la asigna otro departamento. Debajo empieza la lista de nombres por resolver. Captura 3.20Cada fila con el docente leído, la carrera, el código, el grupo, los componentes que cubre (T, P, o los subgrupos de un desdoble) y su resultado. Captura 3.21Los nombres que no coinciden con el catálogo se agrupan; para cada uno se sugieren los cinco más parecidos o se usa el nombre tal cual. - En el código
interpretarCargaDepartamento(Dominio.js:1475) conubicarColumnasCarga(:1402) ycomponentesDeFila(:1448).sugerirDocentes(:1381) ordena porsimilitudNombres: la parte de las palabras que comparten los dos nombres.
-
Aplicar
elige a quién corresponde cada nombre y pulsa Aplicar a la asignación.
Captura 3.22«Carga aplicada: 104 asignaciones y 1 sin sección. Revísalas y guarda.» Aparecen «Cambios sin guardar», la cobertura y las advertencias del balance. Nada se guardó todavía: el guardado, con sus controles de horas, es del bloque 5. - En el código
- El botón «Aplicar» (AsignacionCarga.html:150) quita las asignaciones anteriores de cada componente que trae la hoja y pone las nuevas; lo demás de la carga se conserva. Las filas «fuera del plan» solo las agrega el director de la carrera.
3.5Catálogo de acciones y posibilidades
| Acción | Dónde | Función | Resultado |
|---|---|---|---|
| Revisión automática de Drive | Al abrir la página o el tablero, si la última revisión fue hace más de 5 min | sincronizarFuentes_(false) | Versiones nuevas solo en los planes que cambiaron; avisos |
| Releer Excel | Botón del tablero | obtenerTablero(c, {releer: true}) | Revisión inmediata y catálogo releído; «Excel revisado» |
| Pegar desde Google Sheets | Carga masiva de la oferta | crearRejillaPegado | Rejilla con cabeceras detectadas |
| Soltar un archivo en la rejilla | Rejilla de pegado | alSoltarArchivo | El archivo se dibuja para revisarlo antes de procesar |
| Subir archivo | Opción «Subir archivo» (o soltarlo sobre ella) | leerArchivo → procesar | Se procesa directamente; si hay varias hojas, se elige una |
| Descargar plantilla oficial | Opciones de carga | #op-plantilla | Plantilla_Plan_Ciclo.csv con cabeceras y ejemplo |
| Procesar contenido | Rejilla de pegado | procesar → interpretar | Asignaturas a la grilla; aviso de filas de otras carreras |
| Limpiar | Rejilla de pegado | rejilla.limpiar | Vacía la rejilla |
| Reemplazar contenido | Barra inferior de la grilla | accion("reemplazar") → irACarga | Vuelve a las opciones; el reemplazo se confirma al procesar |
| Carga masiva de asignaciones | Asignar docentes, editando una carga | #btn-doc-masiva | Pantalla de pegado o subida de CargaXDepartamento |
| Previsualizar asignaciones | Esa pantalla | interpretarCargaDepartamento | Tabla de filas con su estado y nombres por resolver |
| Resolver un nombre | Tabla «Docentes que no están en el catálogo» | sugerirDocentes | Se elige un docente sugerido o el nombre tal cual |
| Aplicar o descartar | Pie de la previsualización | #btn-aplicar-previa-doc | Cambia la carga en pantalla (sin guardar) o vuelve a la rejilla |
3.6Código clave comentado
Revisar la carpeta: leer fuera del candado, aplicar dentro
const f = localizarXlsx_(carpeta, avisos); if (!f.carrera) { /* aviso FALTA_CARGA_CARRERA */ return avisos; } const actuales = firmaFuentes_(f); // tipo, id, nombre, modificado const guardadas = leerTabla_(TABLA_.FUENTES); if (fuentesIguales_(guardadas, actuales)) return avisos.concat(avisosGuardados_(guardadas)); try { const lectura = leerOfertaXlsx_(f); // lento: se hace SIN el candado const deLectura = conCandado_(function () { const otraVez = leerTabla_(TABLA_.FUENTES); // ¿otro usuario ya lo procesó? if (fuentesIguales_(otraVez, actuales)) return avisosGuardados_(otraVez); const nuevos = aplicarOferta_(f, lectura); reemplazarFilas_(TABLA_.FUENTES, /* firma de cada archivo + avisos */); return nuevos; }); return avisos.concat(deLectura); } catch (e) { avisos.push(Dominio.aviso("LECTURA_XLSX_FALLIDA", "No se pudieron leer los xlsx de la carpeta: " + e.message)); return avisos; }
Es el patrón de «comprobar dos veces»: se compara la firma antes de leer (para no leer en vano) y otra vez dentro del candado (por si otra ejecución lo procesó mientras tanto). Leer fuera del candado deja que los demás sigan guardando.
Aplicar la oferta plan por plan
Object.keys(r.oferta).forEach(function (sig) { const asignaturas = r.oferta[sig]; const firma = Dominio.hash(Dominio.contenidoCanonico(asignaturas)); const fila = buscarPlan_(planes, ciclo, sig); if (fila && fila.firma_xlsx === firma) return; // esa hoja no cambió const p = estadoDePlan_(fila); const antes = p.version ? contenidoPlan_(ciclo, sig, p.version, p.filaVersion) : []; Dominio.conservarIds(asignaturas, antes); // los docentes siguen a sus grupos const fus = prepararFusiones_(ciclo, sig, antes, asignaturas, [], null, { sinBloqueo: true }); registrarVersionPlan_(ciclo, sig, fila, { estado: p.estado === Dominio.ESTADOS.SIN ? Dominio.ESTADOS.REGISTRADO : p.estado, tipo: p.version ? Dominio.TIPO_VERSION.ACTUALIZACION_XLSX : Dominio.TIPO_VERSION.IMPORTACION_XLSX, usuario: "xlsx: " + f.carrera.getName(), fecha: Dominio.isoPeru(modificado), motivo: "", asignaturas: fus.asignaturas, firma_xlsx: firma }); /* si la fusión tocó otros planes, se guardan y se relee Planes */ });
- La firma del plan es el hash cyrb53 de su
contenidoCanonico(semestre, malla, código, nombre, departamento, observaciones y grupos), así que cambiar un archivo sin cambiar una hoja no crea versiones en esa carrera. - Un plan Sin Iniciar pasa directamente a Registrado v1: la oferta del Excel ya es la oficial.
- La actualización no comprueba bloqueos (
sinBloqueo): si alguien edita ese plan, al guardar recibiráCONFLICTO.
Leer una celda del XML de una hoja
if (ac.t === "s") texto = v != null ? (compartidas[parseInt(v, 10)] || "") : ""; // cadena compartida else if (ac.t === "inlineStr") texto = xlsxTexto_(/* <is>…</is> */); // texto en la celda else if (ac.t === "b") texto = v === "1" ? "TRUE" : v != null ? "FALSE" : ""; // booleano else if (ac.t === "str" || ac.t === "e" || ac.t === "d") texto = v != null ? xlsxDecodificar_(v) : ""; else if (v != null) { // número const formato = ac.s != null ? estilos[parseInt(ac.s, 10)] : null; texto = formato && isFinite(Number(v)) ? xlsxFormatearFecha_(Number(v), formato) // con formato de fecha : xlsxNumero_(v); // como lo muestra «General» }
Excel guarda los textos repetidos una sola vez en sharedStrings.xml y cada celda apunta a su índice. Las fechas son números con un estilo de fecha: por eso se leen los estilos (xlsxEstilos_) para mostrarlas como en Excel. La celda se coloca en su columna por la letra de su referencia («C7» → columna 2).
Del texto pegado a una matriz
const lineas = String(txt).replace(/\r\n?/g, "\n").split("\n").filter(l => l.trim() !== ""); /* El separador se deduce del conjunto del texto, no de la primera línea: un rango pegado puede empezar con un título o una fila en blanco. */ if (lineas.some(l => l.includes("\t"))) return lineas.map(l => l.split("\t").map(c => c.trim())); const cuenta = c => lineas.reduce((n, l) => n + (l.split(c).length - 1), 0); const sep = cuenta(";") > cuenta(",") ? ";" : ","; /* …y se separa respetando las comillas ("a;b" es una sola celda, "" es una comilla) */
Interpretar fila por fila
filas.forEach(f => {
if (carrera && mapa.CARRERA !== undefined) {
const c = val(f, "CARRERA");
if (c && !coincideCarrera(c, carrera)) { ignoradas++; return; } // otra carrera
}
const semBruto = val(f, "SEMESTRE");
const ds = descomponerSemestre(semBruto); // «1-2» → sem 1, grupo 2
const sem = !isNaN(ds.sem) ? ds.sem : (semBruto || "Sin semestre");
const grupoBruto = val(f, "GRUPO");
const grupo = grupoBruto !== "" ? grupoBruto : (!isNaN(ds.grupo) ? ds.grupo : 1);
const codigo = val(f, "CODIGO"), nombre = val(f, "ASIGNATURA");
const clave = codigo || nombre || ("fila-" + (++sinCodigo)); // cómo se agrupa
if (!porClave.has(clave)) porClave.set(clave, { sem, malla: val(f, "MALLA"), codigo, nombre,
dpto: val(f, "DPTO"), obs: val(f, "OBSERVACIONES"), grupos: [] });
const a = porClave.get(clave);
if (sinDuplicados && a.grupos.some(g => String(g.n) === String(grupo))) { duplicadas.push(/*…*/); return; }
a.grupos.push(nuevoGrupo(grupo, val(f, "HT"), val(f, "HP"))); // horas tal cual
});
Clasificar una fila de CargaXDepartamento
for (let r = cab + 1; r < filas.length; r++) { const f = filas[r] || []; if (f.every(vacia) || f.some(c => /^\s*TOTAL/i.test(String(c == null ? "" : c)))) continue; docente = val(f, "DOCENTE") || docente; // celdas combinadas: se arrastra el último /* … carrera, grupo (de GRUPO o de SEMESTRE «1-2»), HT, HP … */ const seccion = (codigo && idx.get(carrera + "|C|" + codigo + "|" + n)) || (nombre && idx.get(carrera + "|N|" + normalizar(nombre) + "|" + n)) || null; const estado = dest.carrera && deCarrera && deCarrera !== dest.carrera ? "otra-carrera" : !seccion ? "fuera" : dest.dpto && seccion.dpto !== dest.dpto ? "otro-dpto" : "ok"; }
3.7Errores, avisos y casos límite
| Aviso | Nivel | Cuándo |
|---|---|---|
| CARPETA_INACCESIBLE | error | No se puede abrir la carpeta de SIS_CARPETA_ID |
| LECTURA_XLSX_FALLIDA | error | El .xlsx no se pudo descomprimir o leer |
| FALTA_CARGA_CARRERA | aviso | La carpeta no tiene CargaXCarrera |
| XLSX_CONVERTIDO | aviso | Un archivo es Hoja de Google: hay que subir el .xlsx sin convertir |
| VARIOS_ARCHIVOS | info | Hay varios del mismo tipo; se usa el más reciente |
| HOJA_DESCONOCIDA | aviso | Una hoja con datos que no corresponde a ninguna carrera |
| HOJA_REPETIDA | aviso | Dos hojas de la misma carrera; se usa la primera |
| FILA_REPETIDA | aviso | Código y grupo repetidos en una hoja; la segunda fila no se lee |
| CATALOGO_FALTA · _SIN_CARPETA · _SIN_CABECERAS · _ILEGIBLE | aviso | Problemas con CargaDeDocentes; se puede asignar escribiendo los nombres |
| Mensaje en el navegador | Cuándo |
|---|---|
| «El contenido pegado está vacío.» | Se pegó solo espacio en blanco |
| «No se pudo sacar nada de «archivo».» | El archivo no tenía ni una celda con contenido |
| «No se encontró ninguna fila con contenido.» | Se procesó, pero ninguna fila era de esta carrera o tenía datos |
| «N filas de otras carreras se ignoraron…» | Nota en la grilla tras procesar |
| «Elige a quién corresponde cada nombre que no está en el catálogo.» | Se pulsó Aplicar con nombres sin resolver |
SIS_CICLO_XLSX.
3.8Preguntas probables
¿Por qué no convertir los .xlsx a Hojas de Google y leerlos con SpreadsheetApp?
Convertir crea otro archivo y puede cambiar formatos (fechas, números). Leer el .xlsx original deja la carpeta como fuente única y los archivos intactos. Como el lector es una función pura, además se prueba con los Excel reales sin Google.
¿Cómo sabe el sistema que un Excel cambió?
En dos niveles. Por archivo: tipo, id y fecha de modificación guardados en Fuentes. Por plan: el hash del contenido de cada carrera se compara con firma_xlsx del índice. Solo los planes cuyo contenido cambió reciben una versión nueva.
Si el Excel cambia, ¿se pierde lo que editó el coordinador?
El plan recibe una versión «actualización desde xlsx» con el contenido del Excel, pero las versiones anteriores siguen en el historial y se pueden restaurar. Los grupos con el mismo código y número conservan su identificador, así que sus docentes los siguen, y la derivación a otro departamento también se conserva.
¿Qué pasa si no hay fila de cabeceras?
deducirColumnas reconoce cada columna por su contenido con las PISTAS. Si ninguna columna se parece lo suficiente a un campo (umbral 0,34), ese campo queda vacío en lugar de adivinar.
¿Por qué «nunca rechaza»? ¿No es peligroso?
Es lo que pide el caso de uso: la validación la hace la persona en la grilla, con los totales y avisos a la vista. El texto que no se entiende se conserva, y nada se guarda hasta pulsar un botón de guardado.
¿Por qué se leen los Excel fuera del candado?
Leer y descomprimir tarda segundos. Con el candado tomado, nadie podría guardar mientras tanto. Por eso se lee fuera y dentro solo se compara la firma de nuevo y se escriben las versiones.
¿Por qué SheetJS se descarga solo al entrar en la carga masiva?
Pesa casi 1 MB y la mayoría de las visitas no lo necesitan. Se pide al entrar en las opciones de carga, para que esté listo cuando se elija un archivo.
¿Cómo empareja los nombres de los docentes?
Primero busca el nombre exacto normalizado (claveDocente). Si no está, sugiere los del catálogo que comparten más palabras: palabras comunes divididas por las del nombre más largo. La persona confirma; nunca se asigna por parecido sin preguntar.
¿Qué pasa con las filas de otro departamento en la carga masiva?
Se omiten. Cada director solo cambia las secciones que le tocan y el servidor, al guardar, conserva las asignaciones de las demás (combinarCarga_, bloque 5).
3.9Para practicar
node --test AppScript/pruebas/lector.test.mjs
Bloque 4
Plan de ciclo (CU-001)
Responsable: jortizc@ulasalle.edu.pe
4.1Ficha del bloque
- Pregunta que responde
- ¿Cómo registra el coordinador el plan de ciclo de su carrera: qué asignaturas y grupos abre, con qué horas, y cómo lo deja listo para asignar docentes?
- Archivos del servidor
Planes.js 353Fusiones.js 62- Archivos del navegador
Tablero.html 310Grilla.html 273GrillaAcciones.html 303GrillaEdicion.html 370- Parte de Dominio.js
L612–648 totalesL649–886 operaciones de ediciónL887–1018 fusiones entre carreras- Usa
- El bloque 2 para el bloqueo, las versiones y la recuperación; el bloque 3 para la carga masiva;
Acceso.jspara los permisos. - Pruebas
dominio.test.mjs(operaciones y totales),servidor.test.mjs(endpoints con Google simulado) yrecorrido-ui.mjs(el recorrido en Chrome).- Idea central
- Todo se edita en el navegador con las funciones de
Dominio, sin llamar al servidor; el servidor solo interviene al abrir, guardar, restaurar o derivar, y vuelve a comprobarlo todo.
4.2Guion de exposición · unos 13 minutos
-
0:00 – 1:00
Qué es un plan de ciclo
«El plan de ciclo es la oferta de una carrera en un ciclo: sus asignaturas por semestre, y de cada asignatura sus grupos con horas de teoría y de práctica. Pasa por tres estados: Sin Iniciar, Borrador y Registrado. Registrarlo habilita la asignación docente. Un plan registrado se sigue pudiendo editar, pero nunca vuelve a borrador, y cada guardado deja una versión.»
Muestra: el diagrama de estados.
-
1:00 – 2:30
Inicio y tablero
«El inicio resume el ciclo: planes registrados, en borrador, sin iniciar, grupos y horas de dictado. El tablero lista las ocho carreras con su estado, totales, versión y quién lo está editando. El botón Editar solo aparece en las carreras que coordina la cuenta. El tablero trae además el contenido vigente de cada plan, así que consultar o empezar a editar no espera al servidor.»
Muestra: capturas 4.1, 4.2 y 4.3.
-
2:30 – 3:30
Consultar en solo lectura
«Al pulsar una fila se consulta el plan: el resumen de nueve cifras y un bloque por semestre. La columna Docente se llena aparte, en segundo plano, con las asignaciones vigentes.»
Muestra: captura 4.4.
-
3:30 – 5:00
Entrar en edición y la grilla
«Editar abre la grilla al instante, mientras el bloqueo se pide de fondo; hasta que llega no se puede guardar. Arriba, la nota del estado y el resumen; después los semestres, que se pueden plegar. Al seleccionar un grupo aparece en el dock inferior con las acciones que le aplican.»
Muestra: capturas 4.5 a 4.7.
-
5:00 – 7:30
Editar sin validar
«Se pueden editar las horas, que se guardan tal como se escriben; editar los datos de la asignatura; agregar asignaturas o grupos, uno o a todo un semestre; y eliminar. Antes de eliminar un grupo que ya tiene docente se avisa: la asignación no se borra, queda pendiente para que el director la reasigne. El desdoble divide la práctica o la teoría de un grupo en dos a cinco subgrupos y se marca, por ejemplo, PRA×3.»
Muestra: capturas 4.8 a 4.15.
-
7:30 – 9:30
Fusiones
«Fusionar une grupos en una sola sección con un docente. Se elige un grupo principal y los demás toman sus horas; las horas de antes se guardan para poder deshacer. Si los grupos difieren en nombre, código u horas, se advierte con A05 pero se fusiona igual. Se puede fusionar con grupos de otra carrera: al guardar, el servidor lleva la fusión a ese otro plan, con una versión de tipo fusión, salvo que alguien lo esté editando. En las horas de dictado, la sección fusionada cuenta una sola vez.»
Muestra: capturas 4.16 a 4.22.
-
9:30 – 11:30
Guardar y registrar
«Hay tres formas de guardar: Guardar borrador, Registrar plan, que muestra el resumen y pide confirmar, y Guardar cambios cuando ya está registrado, con un motivo opcional. Al registrar se ve la pantalla de éxito con tres caminos: volver al tablero, seguir editando o ir a asignar docentes. El historial permite restaurar cualquier versión.»
Muestra: capturas 4.24 a 4.29.
-
11:30 – 12:30
Derivar un curso a otro departamento
«Por regla, los docentes de una carrera los asigna el departamento de esa carrera. La excepción es derivar un curso: el director del departamento que hoy lo asigna, o el coordinador, lo pasa a otro departamento desde la consulta. Se guarda como versión “cambio de departamento”, la columna DPTO del Excel no cambia y las asignaciones siguen al curso.»
Muestra: capturas 4.30 a 4.32.
-
12:30 – 13:00
Cierre
«El navegador edita con el mismo Dominio que usa el servidor; el servidor solo recibe el plan completo al guardar, lo sanea, comprueba permiso, bloqueo y conflicto, reconcilia las fusiones y guarda la versión.»
4.3Conceptos clave
Estados del plan
resolverEstado_ (Planes.js:56): si el plan ya está Registrado, queda Registrado; si no, queda Registrado solo si se pidió registrar, y Borrador en cualquier otro guardado. Un plan Sin Iniciar que llega desde el Excel pasa directo a Registrado v1 (bloque 3). «Bloqueado» no es un estado del plan: es como lo ve otra cuenta mientras alguien lo edita.
El modelo de datos
[
{
"sem": 1, "malla": "2024", "codigo": "3.1.1.24", "nombre": "Introducción a la Programación",
"dpto": "DIMA", // columna DPTO del Excel: solo informativa
"obs": "", // INTERNACIONAL o VIRTUAL la marcan como «Intl.»
"derivado": "DAHUC", // opcional: lo asigna otro departamento
"grupos": [
{ "id": "g8k2…", "n": 1, "ht": "2", "hp": "4",
"desdoble": "PRA×3", // o null
"fusion": null }, // marca «F…» si está fusionado
{ "id": "g1x9…", "n": 2, "ht": "2", "hp": "4", "desdoble": null,
"fusion": "FQ3K7ZB", "fusionPrincipal": true,
"fusionCon": [ { "carrera": "IND", "grupoId": "g7…", "codigo": "17.1.5.24", "n": 1, "principal": false } ],
"previas": { "ht": "3", "hp": "2" } } // horas antes de fusionar, para deshacer
]
}
]
El id de cada grupo es lo que siguen las asignaciones docentes; por eso se conserva al reemplazar el contenido o al actualizar desde el Excel. Las horas se guardan tal como se escribieron (texto o número): solo se interpretan para sumar.
Las nueve cifras del resumen
| Cifra | Cómo se calcula (Dominio.totales, L621) |
|---|---|
| Asignaturas · Grupos | Cuántas asignaturas y cuántos grupos tiene el plan |
| Horas teóricas · prácticas · total | Suma de HT, de HP y de HT+HP de todos los grupos |
| Horas de dictado | Como el total, pero cada sección fusionada cuenta una sola vez; si su grupo principal está en el plan de otra carrera, la cuenta ese plan y este no |
| Intl. / virtual | Asignaturas cuya observación dice INTERNACIONAL o VIRTUAL |
| Fusiones · Desdobles | Marcas de fusión distintas y grupos con desdoble |
Tipos de versión
| Tipo (así queda en el historial) | Lo crea |
|---|---|
| importación desde xlsx | La primera lectura del Excel (bloque 3) |
| actualización desde xlsx | Un Excel que cambió el contenido de esa carrera |
| guardar borrador | «Guardar borrador» |
| registrar | «Registrar plan» |
| guardar cambios | «Guardar cambios» de un plan registrado |
| autoguardado | La sesión de edición expiró con cambios |
| fusión | Otra carrera fusionó, cambió o deshizo una fusión con un grupo de este plan |
| cambio de departamento | Derivar un curso |
| restauración de vN | Restaurar desde el historial |
Quién puede hacer qué
| Cuenta | Consultar | Editar, guardar, registrar, restaurar | Derivar un curso |
|---|---|---|---|
| Coordinador de la carrera | Sí | Sí, en las carreras de su columna «carreras» | Sí, en sus carreras |
| Coordinador de otra carrera | Sí | No | No |
| Director de departamento | Sí | No (SIN_PERMISO) | Solo los cursos que hoy asigna su departamento |
Qué pasa al guardar, en el servidor
exigirUsuario_exigirEdicion_sanearAsignaturassolo campos del modelo, límitesexigirSinBloqueoAjeno_conflictoPlan_prepararFusiones_reconciliar con los otros planesregistrarVersionPlan_guardarFusionesAjenas_borrarRecuperacion_tomarBloqueo_renueva4.4Recorrido con capturas
Usa ?usuario=mfloresl@ulasalle.edu.pe (coordinador de todas las carreras) y, para derivar, ?usuario=jhuamanih@ulasalle.edu.pe (director de DIMA).
-
El inicio
abre la aplicación.
Captura 4.1Saludo con el rol, anillo de planes registrados, cifras del ciclo y los accesos a las dos secciones. El reporte aún está marcado «Próximamente». - En el código
pintarInicio(Inicio.html) calcula todo con las filas del tablero que trajo la página; no llama al servidor.
-
El tablero
entra en Registrar plan de ciclo.
Captura 4.2Ciclo, «Releer Excel», avisos, la barra de avance y una fila por carrera: estado, semestres, asignaturas, grupos, horas de dictado, versión y quién edita. Captura 4.3Una cuenta que solo coordina ING ve el botón Editar únicamente en esa fila; las demás las consulta. - En el código
pintarTablero(Tablero.html:187) ypintarAvance(:225). Si los datos tienen más de 15 s,mostrarTablerolos vuelve a pedir de fondo (VIGENCIA_TABLERO_MS). Cambiar de ciclo vuelve a leer.
-
Consultar un plan
pulsa la fila de Derecho.
Captura 4.4La consulta: estado y versión, el resumen de nueve cifras y un bloque por semestre, con la columna Docente. - En el código
abrirLectura(Tablero.html:245) usa el contenido que trajo el tablero; solo si no lo tiene llama aobtenerPlan. Los docentes llegan aparte conobtenerDocentesPorGrupoy, cuando llegan, se repinta.
-
La grilla de edición
vuelve al tablero y pulsa Editar en Ingeniería de Software.
Captura 4.5La nota de estado (Registrado v1), el resumen, la barra de herramientas y el dock inferior con los botones de guardado. - En el código
pintarGrilla(Grilla.html:177): el botón principal dice «Registrar plan» o «Guardar cambios» según el estado; «Guardar borrador» se desactiva en un plan registrado; los dos esperan a que el bloqueo esté confirmado (p.confirmado).
-
Seleccionar un grupo
pulsa una fila de la grilla.
Captura 4.6La fila queda marcada y el dock describe el grupo: número, código, nombre y horas. Captura 4.7El dock: «Quitar desdoble» y «Deshacer fusión» solo se activan si el grupo tiene desdoble o fusión ( data-req). -
Editar horas y datos de la asignatura
con un grupo seleccionado, pulsa Editar HT / HP y luego Editar asignatura.
Captura 4.8HT y HP se guardan tal cual se escriben; el total se recalcula. Captura 4.9Código, departamento (DPTO), nombre y observaciones. La nota recuerda que DPTO es informativa. -
Agregar una asignatura o grupos
pulsa Agregar asignatura; después selecciona un grupo y pulsa Agregar grupo; prueba también Agregar grupo al semestre.
Captura 4.10Sin validación: se guarda lo que se escriba. El semestre acepta «5» o «1-2». Captura 4.11Un grupo correlativo en todas las asignaturas del semestre, con las horas de su primer grupo. Captura 4.12«Agregar grupo» copia las horas del grupo seleccionado y le da el siguiente número. - En el código
Dominio.agregarAsignatura(L660) reordena por semestre y código;agregarGrupo(L692) yagregarGrupoSemestre(L705) usansiguienteN.
-
Desdoblar
selecciona un grupo, pulsa Desdoblar, elige Práctica y 3 subgrupos.
Captura 4.13Tipo (práctica o teoría) y número de subgrupos, de 2 a SIS_MAX_SUBGRUPOS(5).Captura 4.14«Quitar desdoble» devuelve el grupo a una sola sección. Captura 4.15La marca PRA×3 junto al nombre. En la asignación docente, cada subgrupo de práctica (P1, P2, P3) se asigna por separado. - Validación
- Si el grupo no tiene horas del tipo elegido: «El desdoble no aplica». Si la cantidad no está entre 2 y el máximo: «Cantidad de subgrupos no válida» y se vuelve a preguntar. Son las únicas validaciones del plan, y no rechazan nada guardado.
-
Fusionar grupos del mismo plan
pulsa Fusionar grupos, marca dos grupos con Ctrl y elige el principal.
Captura 4.16La lista incluye los grupos de este plan y, debajo, los de las otras carreras del ciclo. Captura 4.17A05: los grupos difieren del principal. La fusión se crea igual. Captura 4.18El resumen cuenta la fusión y las horas de dictado bajan: la sección fusionada se dicta una vez. Captura 4.19La marca FUS; al pasar el ratón dice con qué grupos está fusionado. El grupo no principal ya muestra las horas del principal. - En el código
Dominio.fusionar(L803) crea la marca «F» + 6 caracteres, guarda las horas previas de los no principales enprevias, les copia las del principal y devuelve las diferencias para A05. Si un grupo ya estaba en otra fusión, la anterior se limpia conlimpiarFusionHuerfana.
-
Deshacer la fusión y eliminar
selecciona un grupo fusionado y pulsa Deshacer fusión; prueba también Eliminar grupo y Eliminar asignatura.
Captura 4.20Cada grupo recupera las horas que tenía antes. Captura 4.21Si el grupo es el único de su asignatura o está fusionado, se explica qué pasará. Captura 4.22Eliminar la asignatura quita todos sus grupos. Si alguno tuviera docente, una nota lo nombra y explica que la asignación queda pendiente. - En el código
- Antes de eliminar se llama a
asegurarDocentesGrupo(Tablero.html:106) para saber si hay docentes asignados. Una fusión que se queda con menos de dos grupos se deshace sola.
-
Fusionar con otra carrera
en el diálogo de fusión marca un grupo de ING (Cálculo I) y uno de IND que está más abajo en la lista (Matemática y álgebra lineal); el principal es el de ING.
Captura 4.23Los grupos de otras carreras aparecen con su sigla. La nota explica que la fusión se llevará a su plan al guardar, sin pedir aprobación. Captura 4.24A05 nombra el grupo de IND, que tiene otro código y otras horas. - En el servidor
prepararFusiones_(Fusiones.js:21) pide aDominio.reconciliarFusiones(L934) cómo quedan las fusiones en todos los planes del ciclo; si alguno de esos planes lo edita otra cuenta, falla conBLOQUEADOy no se guarda nada.guardarFusionesAjenas_(:50) registra en cada plan tocado una versión «fusión».- Regla
- Desde un plan solo se fusionan secciones que incluyan al menos un grupo suyo: si se eligen solo grupos ajenos, se pide volver a seleccionar.
-
Plegar los semestres
pulsa Colapsar todo (o la cabecera de un semestre).
Captura 4.25Cada semestre muestra en su cabecera cuántas asignaturas, grupos y horas tiene. -
Guardar cambios de un plan registrado
pulsa Guardar cambios, escribe un motivo y confirma.
Captura 4.26El motivo es opcional y queda en el historial. Captura 4.27«Cambios guardados como v2» y «La fusión se guardó también en IND (v2)». El reloj volvió a 30:00 porque guardar renueva el bloqueo. -
Registrar un plan nuevo
en un plan en Sin Iniciar o Borrador, pulsa Registrar plan.
Captura 4.28Resumen de cifras y el mensaje del caso de uso: al confirmar se habilita la asignación docente para DIMA, DAHUC, DACJ y DACTE. Captura 4.29«¡Plan registrado!» con la versión y tres caminos. Al registrar se suelta el bloqueo (con movimiento, además, sale confeti). -
Guardar como borrador
en un plan que todavía no está registrado, pulsa Guardar borrador.
Captura 4.30El borrador no habilita la asignación docente. Captura 4.31La nota de estado cambia a Borrador (v1). Captura 4.32El tablero de 2026-2 después: ING Registrado, ADM en Borrador, el resto Sin Iniciar. -
Derivar un curso a otro departamento
con
?usuario=jhuamanih@ulasalle.edu.pe(director de DIMA), consulta ING y pulsa Derivar en un curso.Captura 4.33El director no tiene «Editar», pero ve «Derivar» en los cursos que asigna su departamento. Captura 4.34Departamento que lo asignará y motivo. Elegir el departamento de la carrera quita la derivación. Captura 4.35DPTO sigue diciendo DIMA, pero ahora «Asigna DAHUC». En ese curso el director de DIMA ya no ve «Derivar»: ahora lo asigna otro departamento. - En el código
derivarAsignatura(Tablero.html:299) →cambiarDepartamento(Planes.js:321): comprueba que quien deriva sea el director del departamento que hoy lo asigna o el coordinador de la carrera, que el plan no lo edite otra cuenta, y guarda una versión «cambio de departamento» con el motivo «código nombre: DIMA → DAHUC».
4.5Catálogo de acciones y posibilidades
| Acción | Requiere | Función | Qué hace |
|---|---|---|---|
| Cambiar de ciclo | — | #sel-ciclo → mostrarTablero | Lee el tablero de ese ciclo; el ciclo se comparte con Asignar docentes |
| Consultar un plan | — | abrirLectura | Solo lectura, con docentes por grupo |
| Editar un plan | Coordinar la carrera | abrirEdicion | Grilla con bloqueo; carga masiva si está vacío |
| Colapsar / expandir | — | #btn-colapsar | Pliega o despliega todos los semestres |
| Agregar asignatura | — | Dominio.agregarAsignatura | Asignatura con un grupo; reordena |
| Agregar grupo al semestre | — | Dominio.agregarGrupoSemestre | Un grupo en cada asignatura del semestre seleccionado (o del primero) |
| Fusionar grupos | Dos o más grupos, uno de este plan | Dominio.fusionar | Sección con un grupo principal; A05 si difieren |
| Historial de versiones | Bloqueo confirmado | obtenerHistorial, restaurarVersion | Ver y restaurar versiones |
| Editar HT / HP | Grupo seleccionado | accion("editar") | Cambia las horas tal cual |
| Editar asignatura | Grupo seleccionado | accion("editar-asignatura") | Código, nombre, DPTO y observaciones |
| Agregar grupo | Grupo seleccionado | Dominio.agregarGrupo | Grupo siguiente con las mismas horas |
| Desdoblar / Quitar desdoble | Grupo seleccionado / con desdoble | Dominio.desdoblar, quitarDesdoble | PRA×n o TEO×n, de 2 a 5 |
| Deshacer fusión | Grupo fusionado | Dominio.deshacerFusion | Disuelve la sección; en otras carreras al guardar |
| Eliminar grupo / asignatura | Grupo seleccionado | Dominio.eliminarGrupo, eliminarAsignatura | Avisa si hay docentes o fusiones |
| Reemplazar contenido | — | irACarga | Vuelve a la carga masiva (bloque 3) |
| Guardar borrador | Plan no registrado, bloqueo confirmado | guardarPlan (tipo «guardar borrador») | Versión en Borrador |
| Registrar plan | Bloqueo confirmado | guardarPlan (tipo «registrar») | Versión Registrado, pantalla de éxito, suelta el bloqueo |
| Guardar cambios | Plan registrado | guardarPlan (tipo «guardar cambios») | Versión nueva con motivo opcional |
| Salir de la edición | — | terminarEdicion | Descarta lo no guardado y suelta el bloqueo |
| Derivar un curso | Director del dpto. que lo asigna o coordinador | cambiarDepartamento | Versión «cambio de departamento» |
| Ir a asignar docentes | — | data-ir="docentes" | Desde la pantalla de éxito (bloque 5) |
4.6Código clave comentado
El endpoint de guardado
function guardarPlan(ciclo, sig, datos) { return responder_(function () { // contrato {ok, datos} const usuario = exigirUsuario_(); const c = validarCiclo_(ciclo), s = validarCarrera_(sig); exigirEdicion_(usuario, s); // SIN_PERMISO si no coordina const T = Dominio.TIPO_VERSION; if (!datos || [T.BORRADOR, T.REGISTRAR, T.CAMBIOS, T.AUTOGUARDADO].indexOf(datos.tipo) < 0) fallo_("DATO_INVALIDO", "Tipo de guardado desconocido."); const recibidas = Dominio.sanearAsignaturas(datos.asignaturas); // nunca se confía en el navegador const disueltas = Dominio.sanearMarcas(datos.fusionesDisueltas); return conCandado_(function () { const b = exigirSinBloqueoAjeno_(c, s, usuario, "no se guardó"); const fila = filaPlan_(c, s); const p = estadoDePlan_(fila); const conflicto = datos.forzar ? null : conflictoPlan_(c, s, p.version, datos.versionBase); if (conflicto) fallo_("CONFLICTO", "Mientras editabas se guardó la versión v" + conflicto.n + " del plan.", conflicto); const estado = resolverEstado_(p.estado, datos.estado); // Registrado no vuelve atrás const fecha = Dominio.isoPeru(); const antes = p.version ? contenidoPlan_(c, s, p.version, p.filaVersion) : []; const f = prepararFusiones_(c, s, antes, recibidas, disueltas, usuario); const n = registrarVersionPlan_(c, s, fila, { estado: estado, tipo: datos.tipo, usuario: usuario.correo, fecha: fecha, motivo: String(datos.motivo || "").slice(0, 500), asignaturas: f.asignaturas }); const otros = guardarFusionesAjenas_(c, f.otros, usuario.correo, fecha); borrarRecuperacion_(c, Dominio.REF.plan(s), usuario.correo); const r = b ? tomarBloqueo_(c, Dominio.REF.plan(s), usuario.correo) : null; // renueva return { version: n, estado: estado, fecha: fecha, expira: r && r.ok ? r.expira : null, asignaturas: f.asignaturas, otros: otros }; }); }); }
En una sola función se ven los tres bloques juntos: permisos y saneamiento (plataforma), candado, bloqueo, conflicto y versión (bloque 2), y estado y fusiones (bloque 4). sanearAsignaturas se queda solo con los campos del modelo, recorta textos y rechaza lo desmedido: más de 2000 asignaturas, más de 60 grupos por asignatura o más de 600 000 caracteres de JSON.
Las horas de dictado
(asignaturas || []).forEach(a => a.grupos.forEach(g => {
if (!g.fusion) return;
if (g.fusionPrincipal) principalAqui.add(g.fusion);
if ((g.fusionCon || []).some(x => x.principal)) principalFuera.add(g.fusion);
}));
(asignaturas || []).forEach(a => {
t.asig++; sems.add(a.sem);
if (esInternacional(a)) t.intl++;
a.grupos.forEach(g => {
t.grupos++; t.ht += num(g.ht); t.hp += num(g.hp); t.th += th(g);
if (g.desdoble) t.des++;
if (g.fusion) {
if (!fus.has(g.fusion)) { // la sección cuenta una vez…
fus.add(g.fusion);
if (principalAqui.has(g.fusion) || !principalFuera.has(g.fusion)) t.dictado += th(g); // …en el plan del principal
}
}
else t.dictado += th(g);
});
});
Fusionar: el principal define las horas
locales.forEach(v => {
const s = par(v); // {a, g} de "ci:gi"
const esPrincipal = !!pLocal && s.g === pLocal.g;
if (!esPrincipal && difiereDe(s.a.nombre, s.a.codigo, s.g.ht, s.g.hp, ref))
difs.push(s.a.codigo + " " + s.a.nombre + " (grupo " + s.g.n + ")"); // → advertencia A05
if (s.g.fusion && s.g.fusion !== marca) anteriores.add(s.g.fusion);
if (!esPrincipal && (s.g.ht !== ref.ht || s.g.hp !== ref.hp) && !s.g.previas)
s.g.previas = { ht: s.g.ht, hp: s.g.hp }; // para deshacer
s.g.fusion = marca;
if (esPrincipal) s.g.fusionPrincipal = true; else delete s.g.fusionPrincipal;
if (fusionCon.length) s.g.fusionCon = fusionCon.map(x => Object.assign({}, x)); else delete s.g.fusionCon;
if (!esPrincipal) { s.g.ht = ref.ht; s.g.hp = ref.hp; } // toma las horas del principal
});
anteriores.forEach(m => limpiarFusionHuerfana(asignaturas, m));
Reconciliar fusiones entre carreras
Dominio.reconciliarFusiones(sig, antes, despues, disueltas, otros) (L934) no toca la hoja: recibe el contenido de todos los planes del ciclo y devuelve qué cambia en cada uno. Para cada marca afectada:
- Si el usuario la deshizo y ya no queda ningún grupo suyo, la quita de los otros planes.
- Incorpora a la fusión los grupos de otras carreras que pide
fusionCon(si un grupo ya no existe, se ignora). - Si quedan menos de dos miembros, disuelve la fusión en todos.
- Si no, elige el principal (el marcado, o el pedido) e iguala: los demás toman sus horas, guardan
previasy cada uno lista a los miembros de las otras carreras.
Los cambios se hacen sobre copias, y cada plan tocado anota un motivo («fusión con ING», «fusión disuelta») que queda en su versión.
Validar el desdoble en el navegador
const { tipo: clase, cant } = r(); const horas = num(clase === "Práctica" ? s.g.hp : s.g.ht); const n = parseInt(cant, 10); if (!horas) { await informar("El desdoble no aplica", "El grupo no registra horas de " + clase.toLowerCase() + "…"); return; } if (!(n >= 2 && n <= CONFIG.maxSubgrupos)) { await informar("Cantidad de subgrupos no válida", "…entre 2 y " + CONFIG.maxSubgrupos + "…"); return accion("desdoblar"); // vuelve a preguntar } Dominio.desdoblar(s.g, clase, n); // g.desdoble = "PRA×3"
Permiso para derivar
const propio = Dominio.dptoDeCarrera(s); // ING → DIMA const actual = Dominio.DPTOS.indexOf(a.derivado) >= 0 ? a.derivado : propio; // quién lo asigna hoy const esDirector = usuario.rol === "director" && !!usuario.departamento && usuario.departamento === actual; if (!esDirector && usuario.carreras.indexOf(s) < 0) fallo_("SIN_PERMISO", /* … */); if (actual === d) return { /* … */ sinCambios: true }; exigirSinBloqueoAjeno_(c, s, usuario, "el cambio de departamento no se guardó"); if (d === propio) delete a.derivado; else a.derivado = d; // volver al propio quita la derivación
4.7Errores, avisos y casos límite
| Código o aviso | Cuándo | Qué ve la persona |
|---|---|---|
| SIN_PERMISO | Abrir, guardar o restaurar un plan que no coordina; un director que intenta editar; derivar un curso que no asigna | Mensaje que explica qué sí puede hacer |
| BLOQUEADO | El plan, o un plan que la fusión cambia, lo edita otra cuenta | «No se guardó: la fusión cambia el plan de …, que está editando …» |
| CONFLICTO | Se guardó otra versión mientras se editaba | Diálogo con «Guardar de todos modos» (bloque 2) |
| DATO_INVALIDO | Ciclo no configurado, carrera inexistente, tipo de guardado desconocido, grupo no válido o un plan desmedido | El mensaje del saneamiento |
| NO_ENCONTRADO | Restaurar una versión que no existe; derivar una asignatura que ya no está | «La versión vN no existe.» · «…Actualiza la pantalla.» |
| A05 | Fusión de grupos con distinto nombre, código u horas | Advertencia; la fusión se crea igual |
| «El desdoble no aplica» | El grupo no tiene horas del tipo elegido | No se generan subgrupos |
| «Hacen falta al menos dos grupos para fusionar.» | El ciclo tiene menos de dos grupos | Aviso breve |
| «El plan no tiene asignaturas que guardar.» | Guardar o registrar un plan vacío | Aviso breve; no se llama al servidor |
SIN_SECCION para que el director la reasigne. Si dos carreras se fusionan y una cambia su grupo principal, la sección pasa a la carga de la carrera del nuevo principal. Una fusión con un grupo que ya no existe en su plan se ignora en la reconciliación. La derivación se conserva aunque el Excel actualice el plan.
4.8Preguntas probables
¿Por qué un plan registrado no puede volver a borrador?
Porque registrarlo habilita la asignación docente: los departamentos ya pueden estar asignando sobre él. Volver a borrador dejaría esas asignaciones sobre un plan «no oficial». Se puede seguir editando, y cada cambio queda como versión.
¿Qué valida el sistema al editar?
Casi nada a propósito: advierte (A05, docentes asignados, filas ignoradas) pero no bloquea. Las únicas comprobaciones son el desdoble (que haya horas y de 2 a 5 subgrupos) y, en el servidor, el saneamiento contra datos imposibles de guardar.
¿Por qué el navegador edita sin hablar con el servidor?
Porque cada acción es inmediata y las funciones de Dominio son las mismas en los dos lados. El servidor solo recibe el plan completo al guardar y vuelve a comprobar permiso, bloqueo, conflicto y fusiones: nunca confía en lo que calculó el navegador.
¿Cómo puede una carrera cambiar el plan de otra sin su aprobación?
Solo en lo que toca a la fusión, porque la sección tiene un único docente y debe verse igual en los dos planes. El otro plan recibe una versión «fusión» con el motivo, y si alguien lo está editando, el guardado se detiene con BLOQUEADO.
¿Qué diferencia hay entre DPTO y derivar?
DPTO es la columna del Excel y solo informa. Quien asigna el docente es el departamento de la carrera, salvo que el curso se derive: entonces lo asigna el departamento elegido (asignatura.derivado), aunque el curso sigue en la carga de su carrera.
¿Por qué el tablero trae el contenido de todos los planes?
Para que consultar o empezar a editar sea instantáneo y para ofrecer los grupos de otras carreras al fusionar. Se lee en bloque y desde la caché de versiones, así que no es caro. Lo que sí es caro, los docentes por grupo, se pide aparte y en segundo plano.
¿Por qué «Registrar» está desactivado al abrir la grilla?
La grilla se abre al instante, pero el bloqueo aún no está confirmado. Hasta que el servidor responde no se puede guardar; se puede ir editando.
¿Qué pasa con los docentes si reemplazo el contenido?
Los grupos que siguen existiendo (mismo código y número de grupo) conservan su identificador, y las asignaciones los siguen. Los que desaparecen dejan sus asignaciones «sin sección».
4.9Para practicar
node --test AppScript/pruebas/dominio.test.mjs AppScript/pruebas/servidor.test.mjs
node AppScript/pruebas/recorrido-ui.mjs
En Windows, el recorrido necesita la ruta de Chrome en la variable CHROME, por ejemplo C:/Program Files/Google/Chrome/Application/chrome.exe.
Glosario
| Término | Qué significa en este sistema |
|---|---|
| Plan de ciclo | La oferta de una carrera en un ciclo: asignaturas por semestre con sus grupos y horas. |
| Ciclo | Periodo académico, como 2026-1. Los configurados están en SIS_CICLOS; los Excel son de SIS_CICLO_XLSX. |
| HT · HP · Total | Horas teóricas, horas prácticas y su suma, por grupo y por semana. |
| Horas de dictado | El total contando una sola vez cada sección fusionada. |
| Grupo | Una sección de una asignatura. Lleva un id que siguen las asignaciones docentes. |
| Desdoble | Dividir la práctica o la teoría de un grupo en subgrupos (PRA×n, TEO×n). |
| Fusión | Varios grupos, de una o varias carreras, que se dictan como una sola sección con un docente. El grupo principal define las horas. |
| Derivar | Pasar a otro departamento la asignación del docente de un curso. |
| Versión | Un guardado completo de un plan o de una carga. Nunca se borra. |
| Índice | La pestaña (Planes o Cargas) con la versión vigente de cada documento y su fila. |
| Bloqueo de edición | La reserva de un plan o una carga para una cuenta, con caducidad. |
| Candado | LockService: impide que dos ejecuciones escriban la hoja a la vez. |
| Latido | La renovación del bloqueo que manda el navegador cuando hay actividad. |
| Copia de recuperación | Lo que se edita sin guardar, copiado cada minuto por si la página se cierra. |
| Conflicto | Intentar guardar sobre una versión que ya no es la vigente. |
| Ref | El nombre de un documento bloqueable: plan:ING o carga:ING. |
| Firma | Lo que identifica un contenido para saber si cambió: «tipo|id|fecha» de un archivo, o el hash del contenido de un plan. |
| Interpretar | Convertir una tabla en asignaturas y grupos sin rechazar nada. |
| Sección | En la asignación docente, lo que pide un docente: un grupo o una fusión entera. |
| Componente | Parte de una sección que se asigna por separado: T, P, T1…, P1…, o C cuando no hay horas. |
| Endpoint | Función del servidor sin «_» final, que el navegador llama con google.script.run. |
| Dominio | Dominio.js: la lógica pura que se ejecuta igual en el servidor y en el navegador. |