Alta de una instancia en SIENEP
Requisitos funcionales del recorrido
RF10Registro de InstanciasRF11Categorizar InstanciasRF13Integración de Instancias con Google CalendarRF14Generación de Identificador de InstanciaRF15Notificación de ID de Instancia
Para RF13 y RF15, la guía muestra cómo el alta prepara la sincronización con Calendar y la notificación del identificador.
Fuente oficial Requerimientos de Programación de UTEC, 2026 · p. 4
Vamos a recorrer el alta de una instancia por un funcionario, desde que abre SIENEP hasta que ve “Instancia creada”, usando el código que tenemos.
Los fragmentos son de nuestras copias revisadas. Cuando reduzco alguno, lo indico para que puedas distinguir la explicación del archivo completo.
1. El funcionario abre SIENEP
Ingresa, por ejemplo, a:
https://sienep-pwa.pages.dev/instanciasEl navegador solicita a Cloudflare Pages los archivos del frontend: HTML, JavaScript y CSS.
Nuestro HTML tiene este elemento:
<div id="root"></div>Es el contenedor donde aparecerá la aplicación. Todavía no contiene el listado de instancias: ese contenido se construye al ejecutar React.
En producción, Vite ya transformó nuestros archivos TypeScript y TSX en JavaScript que el navegador puede ejecutar.
2. Se ejecuta el punto de entrada y se monta la aplicación
Esto ocurre en main.tsx:
const contenedor = document.getElementById('root')
if (!contenedor) {
throw new Error('No se encontró el elemento #root para montar la aplicación.')
}Parte por parte:
documentrepresenta el documento HTML que cargó el navegador.getElementById('root')busca nuestro contenedor.- La comprobación evita continuar si ese elemento no existe.
Después:
createRoot(contenedor).render(
<StrictMode>
<App />
</StrictMode>,
)createRoot prepara ese contenedor para React. render le indica qué componente debe montar: App.
StrictMode activa comprobaciones adicionales durante el desarrollo. No representa una pantalla ni un permiso del sistema.
3. App comprueba la configuración y muestra el enrutador
En App.tsx, primero se comprueba la configuración:
const configuracion = useMemo(() => resolverConfiguracion(), [])
if (!configuracion.ok) {
return <PantallaErrorConfiguracion mensaje={configuracion.mensaje} />
}Esa revisión incluye la dirección de la API, configurada mediante VITE_API_BASE_URL.
Si la configuración es válida, aparece:
<RouterProvider router={enrutador} />Ese componente permite que nuestra aplicación muestre una página diferente según la URL.
Cloudflare entrega el frontend; la dirección configurada de la API indica a dónde se pedirán o enviarán los datos.
4. El funcionario abre “Nueva instancia” y se comprueban sus accesos
Al navegar a /instancias/nueva, el enrutador encuentra esta definición en rutas.tsx:
{
element: (
<RutaConPermiso
permiso="instancias.crear"
lecturas={['estudiantes.leer', 'categorias_instancia.leer']}
/>
),
children: [{
path: '/instancias/nueva',
element: <PaginaNuevaInstancia />,
handle: {
titulo: 'Nueva instancia',
modal: true,
fondoModal: '/instancias',
},
}],
}Cada parte tiene una función:
pathidentifica la URL.elementindica qué componente mostrar.RutaConPermisocomprueba el permiso para crear y los permisos de lectura que necesita el formulario.handlecontiene información que nuestra interfaz usa para presentar el título y el comportamiento modal.
Antes de este control también interviene RutaProtegida, que comprueba la sesión. Si no hay una sesión autenticada, dirige al ingreso.
Esto explica un problema que tuvimos: tener permiso para crear instancias no alcanza para usar el formulario si falta el permiso para consultar las categorías.
Si SIENEP ya estaba abierta, los pasos de carga inicial anteriores ya ocurrieron. React Router puede cambiar de pantalla sin volver a cargar toda la aplicación.
5. El formulario prepara su estado y consulta las opciones
Se monta PaginaNuevaInstancia.tsx.
Allí tenemos:
const [datos, setDatos] = useState<DatosFormularioInstancia>(DATOS_INICIALES)
const [errores, setErrores] = useState<ErroresFormularioInstancia>({})
const [enviando, setEnviando] = useState(false)
const [creada, setCreada] = useState<InstanciaResponse | null>(null)Estas variables representan lo que está pasando en la pantalla:
datos: lo que el funcionario completó.errores: los problemas detectados en los campos.enviando: si hay un envío en curso.creada: la respuesta de un alta exitosa; comienza ennull.
Mediante efectos, el formulario consulta estudiantes y categorías.
Para los estudiantes, además filtra:
const disponibles = respuesta.filter(
(estudiante) => estudiante.estActivo && estudiante.terminosAceptados,
)Para las categorías conserva las activas:
const activas = respuesta.filter((cat) => cat.estActivo)Las consultas del backend ya deben respetar sus reglas de acceso. Estos filtros de pantalla agregan condiciones de disponibilidad para la selección.
6. El funcionario completa los campos
Cuando cambia un campo, nuestra función actualizar modifica el estado:
setDatos((actuales) => ({ ...actuales, [campo]: valor }))Por ejemplo, al escribir el título:
actualescontiene el formulario hasta ese momento....actualesconserva los demás campos.[campo]: valorreemplaza el campo que cambió.
React recibe el nuevo estado y actualiza la interfaz.
Escribir en el formulario todavía no guarda una instancia en PostgreSQL. Los valores están en el estado del frontend.
7. Al presionar Crear, se valida el formulario
El formulario conecta su envío con nuestra función:
<form onSubmit={enviar} noValidate>La función comienza así:
const enviar = async (evento: FormEvent<HTMLFormElement>) => {
evento.preventDefault()
if (envioRef.current !== null) returnpreventDefault() evita el envío HTML tradicional, que recargaría o navegaría la página. Nuestro código controla la petición.
La segunda comprobación evita iniciar otro envío mientras ya existe uno en curso.
Después valida los datos:
const validacion = validarFormularioInstancia(datos, {
categoriasValidas: idsValidos,
})Comprueba, entre otras cosas, título, categoría, estudiante, fecha, hora, descripción y formato del enlace de reunión cuando se completa.
Si algo está mal, muestra los errores y lleva el foco al campo correspondiente. No continúa con el alta.
También combina la fecha y la hora usando nuestra función:
fecHora = combinarFechaHoraUruguay(datos.fecha, datos.hora)Así prepara una fecha con el tratamiento horario previsto para Uruguay.
8. Se construye el objeto que se enviará al backend
Si la validación pasa, el formulario prepara:
const payload: InstanciaCreatePayload = {
titulo: datos.titulo.trim(),
fecHora,
descripcion: datos.descripcion.trim(),
idCategoria: idCategoriaNum,
idEstudiante: Number(datos.idEstudiante),
...(lugar ? { lugar } : {}),
...(urlMeet ? { urlMeet } : {}),
}payload es el cuerpo de la solicitud.
trim() elimina espacios al comienzo y al final. Number() transforma el ID seleccionado en un número. Las últimas dos líneas agregan lugar y enlace solamente cuando tienen contenido.
Un ejemplo ficticio del JSON resultante sería:
{
"titulo": "Seguimiento académico",
"fecHora": "2026-11-15T12:00:00-03:00",
"descripcion": "Revisión del avance acordado.",
"idCategoria": 3,
"idEstudiante": 501,
"lugar": "Sala 2"
}El formulario no envía un funcionario elegido libremente como creador. El backend lo obtiene de la sesión.
9. La función de API y el cliente HTTP envían la solicitud
La página llama a crearInstancia. En instanciasApi.ts tenemos:
export async function crearInstancia(
datos: InstanciaCreatePayload,
opciones?: OpcionesCrearInstancia,
): Promise<InstanciaResponse> {
const respuesta = await clienteHttp.post<unknown>('/api/instancias', datos, {
senal: opciones?.senal,
})
return validarInstanciaResponse(respuesta)
}Esta función conoce el endpoint del módulo. El cliente HTTP compartido resuelve las tareas comunes:
- Construye la URL usando la dirección configurada del backend.
- Convierte el objeto a JSON mediante
JSON.stringify. - Agrega
Content-Type: application/json. - Adjunta la credencial como
Authorization: Bearer …. - Ejecuta
fetchy procesa la respuesta.
La petición de alta es:
POST /api/instanciasawait permite esperar su resultado sin bloquear toda la interfaz del navegador.
10. Spring comprueba la identidad y recibe los datos
En el backend, los filtros de seguridad procesan la credencial. El camino de validación depende de si se ingresó con un JWT local o con Auth0.
Si corresponde un JWT local, también se comprueba su vigencia mediante el mecanismo de sesiones que implementamos.
Con la identidad resuelta, entra en InstanciaController.java:
@PreAuthorize("hasAuthority('instancias.crear')")
@PostMapping
public ResponseEntity<InstanciaResponseDto> crear(
@Valid @RequestBody InstanciaCreateDto dto,
Authentication auth) {
Integer idFuncionario = (Integer) auth.getPrincipal();
return ResponseEntity.status(HttpStatus.CREATED).body(
instanciaService.crearDto(dto.getTitulo(), dto.getFecHora(), dto.getDescripcion(),
idFuncionario, dto.getIdCategoria(), dto.getIdEstudiante(),
dto.getLugar(), dto.getUrlMeet(), esAdmin(auth))
);
}Parte por parte:
@PostMapping: atiende el alta mediante POST, bajo la ruta de clase/api/instancias.@RequestBody: convierte el JSON recibido en un DTO Java.@Valid: aplica las restricciones del DTO.@PreAuthorize: exige el permiso de creación en el servidor.Authentication: contiene la identidad autenticada.auth.getPrincipal(): obtiene el ID del actor.instanciaService.crearDto(...): delega las reglas y el alta al servicio.HttpStatus.CREATED: prepara una respuesta HTTP 201 cuando la operación termina correctamente.
El backend valida aunque el frontend ya lo haya hecho. Una persona podría llamar directamente a la API sin usar nuestro formulario.
11. El servicio comprueba las reglas, genera el código y guarda
Esto ocurre en InstanciaService.java.
El servicio busca al funcionario, comprueba la categoría y verifica que el estudiante esté disponible. Para un funcionario que no es administrador, también calcula su alcance.
El código rechaza un estudiante fuera de ese alcance:
if (!enScope) {
throw new AccessDeniedException("El estudiante no pertenece a tu scope");
}Después asigna el identificador y persiste:
instancia.setCodigoInstancia(generadorCodigo.siguiente());
instancia = instanciaRepository.save(instancia);
autoAgregarCreadorComoParticipante(instancia, funcionario.getIdUsuario());Aquí suceden tres cosas diferentes:
- El generador asigna un código como
INST-2026-000120, usando un contador anual controlado en la DB. - El repositorio persiste la entidad.
- El creador queda incorporado como participante.
Nuestra entidad está vinculada con la tabla:
@Entity
@Table(name = "instancias")
public class Instancia {JPA/Hibernate convierte el trabajo con esa entidad en operaciones SQL sobre PostgreSQL.
@Transactional establece la transacción: los cambios de esa operación deben confirmarse juntos. Si ocurre un error que la revierte, no se confirma el alta a medias.
12. Se preparan las notificaciones y la integración externa
Dentro del alta, el servicio también llama a:
googleCalendarService.crearDesdeInstancia(instancia);
notificacionService.notificarIdInstancia(instancia);El primer servicio prepara y guarda el evento local de calendario, inicialmente sin sincronizar. El segundo registra las notificaciones internas para los destinatarios correspondientes.
La sincronización con Google y el envío de correo se programan para después del commit, según la configuración de las integraciones. La auditoría del alta también tiene su procesamiento posterior al commit.
Por eso hay que distinguir:
Instancia guardada → evento local y avisos preparados → procesamiento externo.
Ver “Instancia creada” confirma el alta local. La recepción del correo o la existencia del evento en Google requieren comprobar esos resultados por separado.
13. La respuesta vuelve al navegador y aparece la confirmación
El backend construye InstanciaResponseDto y lo serializa como JSON.
El frontend valida la respuesta. En el flujo de alta comprueba especialmente que existan un ID y un código válidos.
Entonces la página ejecuta:
setCreada(respuesta)
onCreada?.(respuesta)setCreada guarda el resultado en el estado de React. onCreada, si fue proporcionada, avisa al componente que abrió el formulario.
Al cambiar el estado, React muestra la confirmación que tenemos programada:
<h2>Instancia creada</h2>
<p>La instancia se registró correctamente.</p>Y presenta el código recibido:
Código: <strong>{creada.codigoInstancia}</strong>Si el servidor responde con un error, la página muestra el mensaje correspondiente y termina el estado de envío.
Para explicarlo oralmente, podés decir:
“El funcionario abre el formulario y React comprueba que tenga los permisos necesarios. La pantalla consulta estudiantes y categorías, mantiene los campos en su estado y valida lo completado. Al confirmar, envía un POST con JSON y la credencial de sesión. El backend verifica la identidad, el permiso y el alcance sobre el estudiante. Después genera el código y guarda la instancia mediante JPA dentro de una transacción. También prepara las notificaciones y la integración con Google. Finalmente devuelve la instancia creada y React muestra la confirmación con su código.”