Alta de una instancia en SIENEP

Requisitos funcionales del recorrido

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:

URL
https://sienep-pwa.pages.dev/instancias

El navegador solicita a Cloudflare Pages los archivos del frontend: HTML, JavaScript y CSS.

Nuestro HTML tiene este elemento:

HTML
<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:

TypeScript
const contenedor = document.getElementById('root')

if (!contenedor) {
  throw new Error('No se encontró el elemento #root para montar la aplicación.')
}

Parte por parte:

Después:

TSX
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:

TSX
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:

TSX
<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:

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:

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:

TSX
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:

Mediante efectos, el formulario consulta estudiantes y categorías.

Para los estudiantes, además filtra:

TypeScript
const disponibles = respuesta.filter(
  (estudiante) => estudiante.estActivo && estudiante.terminosAceptados,
)

Para las categorías conserva las activas:

TypeScript
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:

TypeScript
setDatos((actuales) => ({ ...actuales, [campo]: valor }))

Por ejemplo, al escribir el título:

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:

TSX
<form onSubmit={enviar} noValidate>

La función comienza así:

TypeScript
const enviar = async (evento: FormEvent<HTMLFormElement>) => {
  evento.preventDefault()
  if (envioRef.current !== null) return

preventDefault() 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

JSON
{
  "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:

TypeScript
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:

La petición de alta es:

HTTP
POST /api/instancias

await 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:

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:

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:

Java
if (!enScope) {
    throw new AccessDeniedException("El estudiante no pertenece a tu scope");
}

Después asigna el identificador y persiste:

Java
instancia.setCodigoInstancia(generadorCodigo.siguiente());
instancia = instanciaRepository.save(instancia);
autoAgregarCreadorComoParticipante(instancia, funcionario.getIdUsuario());

Aquí suceden tres cosas diferentes:

Nuestra entidad está vinculada con la tabla:

Java
@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:

Java
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:

TypeScript
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:

HTML
<h2>Instancia creada</h2>
<p>La instancia se registró correctamente.</p>

Y presenta el código recibido:

TSX
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.”