v1.2.3: Fix SIU SSL, matching de comisiones/equipos, credencial QR ampliada y actualizacion de beneficios

This commit is contained in:
2026-08-28 14:12:44 -03:00
parent b2d31db5a8
commit 29284ae131
13 changed files with 741 additions and 183 deletions
+149 -91
View File
@@ -1,136 +1,194 @@
# Aulas UNICABA
# Aulas UNICABA 🎓📱
Un visor híbrido especializado y offline-first desarrollado para la visualización de la cartelera oficial de aulas, seguimiento automatizado de materias cursadas y generación de la Credencial Digital Universitaria para estudiantes de la Universidad de la Ciudad (UNICABA).
[![Version](https://img.shields.io/badge/version-1.2.3-blue.svg)](package.json)
[![Angular](https://img.shields.io/badge/Angular-20.3-red.svg)](https://angular.io/)
[![Ionic](https://img.shields.io/badge/Ionic-v8-blue.svg)](https://ionicframework.com/)
[![Capacitor](https://img.shields.io/badge/Capacitor-v8.2-119EFF.svg)](https://capacitorjs.com/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
Un ecosistema móvil híbrido, especializado y **offline-first** desarrollado para la comunidad académica de la **Universidad de la Ciudad (UNICABA)**. Centraliza la cartelera oficial de horarios y aulas, el seguimiento de la carrera académica, la agenda de exámenes, la integración con SIU Guaraní y Campus Virtual Moodle, la Credencial Digital QR y el acceso a beneficios estudiantiles.
---
## ✨ Características
## ✨ Características Principales (v1.2.3)
### 📋 Cartelera de Horarios
- Visualización completa de la cartelera oficial de aulas organizada por día de la semana (Lunes a Sábado).
- **Modo offline-first**: la cartelera se cachea localmente al primer acceso y se muestra instantáneamente aunque no haya conexión.
- **Sincronización automática** en background (Background Fetch) delegada al sistema operativo.
- **Búsqueda fuzzy** tolerante a errores tipográficos: busca por nombre de materia, código, aula o carrera.
- **Filtro por vista**: alternás entre "Favoritos" y "Cartelera completa" con un segmento.
- Acceso directo a clases virtuales (Zoom / Meet) desde el botón de cada materia.
- Función de compartir el horario de una materia por WhatsApp, Telegram, y cualquier app nativa.
### ⭐ Favoritos Inteligentes
- Marcá hasta 5 materias como favoritas para tenerlas a mano en su propia pestaña.
- Los favoritos se guardan automáticamente en un archivo `aulas_unicaba_datos.csv` en la carpeta "Documentos" del dispositivo, **sobreviviendo a desinstalaciones y actualizaciones** gracias a las políticas de almacenamiento de Android 11+.
- **Alertas locales automáticas**: si una materia favorita cambia de aula o link de clase virtual, la app lo detecta en el próximo sincronismo y dispara una notificación push inmediata.
- Restauración manual de favoritos desde un respaldo CSV (Google Drive, iCloud, Descargas).
### 🎓 Banner "Próxima Clase" *(Nuevo en v1.1.6)*
- Al entrar a la pestaña **Favoritos**, aparece automáticamente un banner destacado con la materia favorita cuya clase es la más cercana en el tiempo (hoy o mañana).
- **Chip de countdown** en tiempo real que se actualiza cada minuto:
- 🔵 Azul → más de 2 horas
- 🟡 Amarillo → entre 30 minutos y 2 horas
- 🔴 Rojo + animación de pulso → menos de 30 minutos
- Muestra nombre de la materia, número de aula y día + hora de inicio.
### 🔔 Recordatorio Pre-Clase *(Nuevo en v1.1.6)*
- Cada vez que la app sincroniza la cartelera, programa automáticamente una notificación local **30 minutos antes** del inicio de cada materia favorita del día.
- Usa IDs de notificación estables para evitar duplicados al sincronizar múltiples veces.
- Funciona completamente **offline** — no requiere servidor externo.
### 📅 Agenda de Hitos y Exámenes *(Nuevo en v1.1.8)*
- Añade exámenes (Parciales, Finales) y Trabajos Prácticos directamente desde el menú de una materia.
- Los hitos muestran insignias de tiempo (rojo cuando falta poco, secundario el resto del tiempo) en la tarjeta de la materia en Cartelera.
- La selección de fecha se hace mediante un popover ligero e intuitivo.
### 📍 Integración Nativa con Mapas *(Mejora v1.1.9)*
- El pie de página renovado con un rediseño UI tipo "botón flotante".
- Utiliza esquemas nativos (`geo:0,0?q=` en Android y `maps://` en iOS) que dejan al usuario **elegir la app de navegación preferida** (Waze, Google Maps, Apple Maps) al tocar la sede.
### 🪪 Credencial Digital Universitaria
- Inicio de sesión directo al Campus Virtual (Moodle) con DNI y clave.
- **Sin intermediarios**: las credenciales nunca pasan por ningún servidor tercero. El flujo HTTP se delega al motor nativo del SO mediante `CapacitorHttp`, evitando bloqueos CORS.
- Descarga y renderiza el PDF de la credencial (código QR) como imagen nativa usando `pdfjs-dist`.
- La imagen queda **cacheada localmente** para usarla offline sin volver a loguearse.
- El brillo de la pantalla aumenta automáticamente al mostrar el QR para facilitar su lectura.
- Login completamente **opcional** — la cartelera funciona sin necesidad de cuenta.
### 🛡️ Privacidad y Seguridad
- No se recopilan nombres, contraseñas ni correos.
- No hay publicidad, telemetría ni analytics de terceros.
- Todo el procesamiento ocurre en la memoria del teléfono.
### 📋 Cartelera de Horarios y Aulas
- **Visualización organizada**: consulta de la cartelera oficial de horarios por día de la semana (Lunes a Sábado).
- **Modo offline-first**: la cartelera se almacena en caché local y se encuentra disponible instantáneamente sin requerir conexión a internet.
- **Búsqueda fuzzy en tiempo real**: motor de búsqueda tolerante a errores ortográficos por nombre de materia, código oficial, número de aula o carrera.
- **Segmentos de visualización**: alternancia rápida entre *Favoritos*, *Cartelera completa* y *Mi Carrera*.
- **Acceso directo a Aulas Virtuales**: botón integrado para ingresar a clases remotas en Zoom o Google Meet.
- **Compartir horario**: exportación nativa de los datos de cursada a WhatsApp, Telegram y otras aplicaciones del sistema (`@capacitor/share`).
- **Sincronización en background**: actualización periódica y desatendida delegada al sistema operativo mediante Background Fetch.
---
## 🛠️ Información Técnica y Frameworks
### ⭐ Favoritos Inteligentes & Respaldo Resiliente
- **Diferenciación precisa de Equipos/Comisiones (Nuevo en v1.2.3)**: al sincronizar con SIU Guaraní o seleccionar manualmente, el sistema identifica y marca **únicamente el equipo o comisión asignado** (ej: `Equipo 1` vs `Equipo 2`), evitando duplicados cuando las materias comparten el mismo código `ASIG`.
- **Persistencia anti-wipes**: los favoritos y datos se respaldan automáticamente en un archivo `aulas_unicaba_datos.csv` ubicado en la carpeta pública "Documentos" del dispositivo, **sobreviviendo a desinstalaciones y actualizaciones** según las políticas *Storage Scoped* de Android 11+.
- **Alertas automáticas de cambio de aula**: detección en segundo plano de modificaciones en aulas o links virtuales con emisión de notificaciones push locales inmediatas.
- **Gestor de respaldo y restauración**: exportación manual y restauración desde archivos CSV almacenados en el dispositivo o la nube (Google Drive, iCloud, Descargas).
| Componente | Tecnología |
---
### 🎓 Banner "Próxima Clase" con Cuenta Regresiva
- **Detección inteligente**: identifica automáticamente la materia favorita con cursada más próxima en el tiempo (hoy o mañana) filtrada por tu equipo específico.
- **Chip dinámico de cuenta regresiva** actualizado minuto a minuto:
- 🔵 **Azul**: más de 2 horas para el inicio.
- 🟡 **Amarillo**: entre 30 minutos y 2 horas.
- 🔴 **Rojo con animación de pulso**: menos de 30 minutos.
- Acceso directo al detalle y agenda de la materia al tocar el banner.
---
### 🔔 Recordatorios Pre-Clase Automáticos
- **Notificaciones locales 30 min antes**: cada sincronización de cartelera programa alertas 30 minutos antes del inicio de cada materia favorita del día.
- **Identificadores estables**: previene notificaciones duplicadas en sincronizaciones sucesivas.
- **Operatividad 100% offline**: funciona localmente en el dispositivo sin depender de servidores de notificaciones externos.
---
### 📅 Agenda de Hitos, Exámenes y Google Calendar
- **Registro de fechas clave**: gestión de Parciales (1 y 2), Trabajos Prácticos (TP 1, 2 y 3), Recuperatorios y Finales directamente desde la tarjeta de cada materia.
- **Insignias de proximidad**: badges visuales en la cartelera con código de color según la cercanía del examen o entrega.
- **Integración con Google Calendar**: sincronización nativa directa del hito con la app de calendario del dispositivo mediante un solo toque (`@ebarooni/capacitor-calendar`).
---
### 🗺️ Mi Carrera (Plan de Estudios y Recorrido Interactivo)
- **Grilla cuatrimestral interactiva**: seguimiento visual del progreso de la carrera para:
- *Licenciatura en Tecnologías Digitales*
- *Licenciatura en Ciencia de Datos*
- *Licenciatura en Desarrollo de Videojuegos*
- **Estados de materias**: marcación rápida de materias como **Aprobadas** (fucsia) o **En Curso** (celeste).
- **Materias optativas**: selección dinámica de asignaturas optativas para completar el trayecto académico.
- **Sincronización con SIU Guaraní**: visualización de notas oficiales y estados de cursada reflejados directamente sobre las celdas del plan de estudios.
---
### 🏫 Integración con SIU Guaraní
- **Autenticación directa y segura**: conexión HTTPS oficial (`https://autogestion.udelaciudad.edu.ar/caba`) con certificados SSL válidos emitidos por Sectigo CA, eliminando errores de validación de cadena.
- **Extracción de Cursadas Activas**: lectura directa de las inscripciones del período lectivo y asignaciones estructuradas (comisión, día, horario y aula).
- **Sincronización de historia académica**: importación automática de materias aprobadas, cursadas regulares y calificaciones oficiales.
- **Certificado de Alumno Regular**: solicitud y descarga automatizada del certificado oficial en formato PDF con apertura nativa en el dispositivo (`@capacitor-community/file-opener`).
---
### 🪪 Credencial Digital Universitaria & Perfil Moodle
- **Perfil unificado**: inicio de sesión con el Campus Virtual Moodle para extraer automáticamente el nombre del estudiante y la fotografía oficial de perfil institucional.
- **Código QR 30% más grande y nítido (Nuevo en v1.2.3)**: diseño optimizado para ocupar todo el ancho disponible del celular, con renderizado PDF a escala 3.0 en alta definición para un escaneo instantáneo en molinetes de acceso.
- **Caché permanente**: credencial disponible en todo momento sin requerir conexión a internet.
- **Brillo automático**: ajuste temporal de la pantalla al 100% de brillo al abrir la credencial para facilitar la lectura del código QR (`@capacitor-community/screen-brightness`).
---
### 🎁 Comunidad de Beneficios UdelaCiudad
- **Descuentos exclusivos en comercios y servicios**:
- 🏋️‍♂️ **ON FIT**: 25% de descuento en membresía mensual con acceso a las 20 sedes en todo el AMBA (máquinas Technogym, plan personalizado, clases grupales y lockers).
- ☕ **BERESHIT (Cafetería)**: 10% de descuento en productos (Tte. Gral. Perón 893, CABA).
- 🍫 **Café & Chocolate**: 10% de descuento en cafetería y delicatessen (Bartolomé Mitre 807, CABA).
- 🍝 **Al Buen Tallarín de Pablo**: 10% de descuento en fábrica de pastas artesanales (Tucumán 1596, CABA).
- 📚 **Atlas Comercial (Librería)**: 10% de descuento en productos de librería y papelería (Bartolomé Mitre 839, CABA).
- 🧘 **Fluire Studio (Pilates)**: 10% de descuento abonando 1 mes y 20% abonando 4 meses (Esmeralda 135, piso 4 B, CABA).
- 🚌 **SOY Estudiante (Transporte)**: instructivo para obtener descuentos en pasajes de micros de larga distancia con tu Certificado de Alumno Regular.
---
### 📖 Manual de Usuario y Centro de Ayuda
- **Guía interactiva integrada**: sección accesible desde el encabezado con instrucciones paso a paso para el uso de la cartelera, seguimiento de carrera, agenda y respaldos.
---
### 📍 Integración Nativa con Mapas
- **Acceso a sede**: botón flotante interactivo en el pie de página con la dirección de la sede institucional (Tte. Gral. J.D. Perón 802, CABA).
- **Compatibilidad multiplataforma**: apertura mediante esquemas nativos (`geo:` en Android y `maps://` en iOS) que permiten elegir entre Google Maps, Waze y Apple Maps.
---
## 🛠️ Tecnologías y Arquitectura
| Componente | Tecnología | Versión |
|---|---|---|
| **Frontend Framework** | [Angular](https://angular.io/) | `20.3.x` |
| **Componentes UI** | [Ionic Framework](https://ionicframework.com/) | `v8.0.x` |
| **Runtime Nativo** | [Capacitor](https://capacitorjs.com/) | `v8.2.0` |
| **Procesamiento PDF** | [PDF.js](https://mozilla.github.io/pdf.js/) | `5.5.x` |
| **Iconografía** | [Ionicons](https://ionic.io/ionicons) | `7.0.x` |
### Plugins de Capacitor
| Plugin | Finalidad |
|---|---|
| Base Frontend | [Angular 20.3.x](https://angular.io/) |
| UI Components | [Ionic Framework v8](https://ionicframework.com/) |
| Puente Nativo | [Capacitor v8](https://capacitorjs.com/) |
| Renderizado PDF | `pdfjs-dist` |
### Plugins de Capacitor utilizados
| Plugin | Uso |
|---|---|
| `@capacitor/filesystem` | Escritura y lectura del CSV de respaldo en "Documentos" |
| `@capawesome/capacitor-file-picker` | Importación manual de CSV desde el explorador de archivos |
| `@capacitor/local-notifications` | Avisos de cambio de aula y recordatorios pre-clase |
| `@capacitor/preferences` | Caché rápida de horarios y credencial |
| `@transistorsoft/capacitor-background-fetch` | Sincronización silenciosa en background |
| `@capacitor-community/screen-brightness` | Máximo brillo al mostrar la credencial QR |
| `@capacitor/haptics` | Feedback háptico en acciones clave |
| `@capacitor/share` | Compartir horarios por WhatsApp, Telegram, etc. |
| `@capacitor/network` | Detección de estado de conexión |
| `@capacitor/clipboard` | Acceso al portapapeles del sistema |
| `@capacitor/filesystem` | Lectura y escritura de respaldos CSV en la carpeta pública "Documentos". |
| `@capawesome/capacitor-file-picker` | Selector de archivos para importación manual de respaldos. |
| `@capacitor/local-notifications` | Notificaciones locales de cambios de aula y recordatorios pre-clase. |
| `@capacitor/preferences` | Almacenamiento seguro en clave-valor para credenciales, caché y estados. |
| `@transistorsoft/capacitor-background-fetch` | Tareas en segundo plano para sincronización automática de cartelera. |
| `@capacitor-community/screen-brightness` | Maximización automática del brillo para escaneo del QR. |
| `@capacitor-community/file-opener` | Apertura nativa del Certificado de Alumno Regular en PDF. |
| `@ebarooni/capacitor-calendar` | Exportación e inserción de hitos y exámenes en el calendario nativo. |
| `@capacitor/haptics` | Respuesta háptica en interacciones táctiles clave. |
| `@capacitor/share` | Diálogo nativo para compartir horarios por mensajería. |
| `@capacitor/network` | Monitorización del estado de conectividad a internet. |
| `@capacitor/clipboard` | Utilidades de portapapeles. |
---
## 📱 Compatibilidad Multiplataforma
## 🔒 Privacidad Zero-Trust (Cero Intermediarios)
Todo el código fuente (TypeScript, HTML y SCSS) y los plugins seleccionados son **100% compatibles con Android e iOS**. La arquitectura garantiza comportamiento idéntico en ambas plataformas.
1. **Conexiones directas punto a punto**: todas las solicitudes HTTP (Google Sheets, Campus Moodle y SIU Guaraní) son emitidas directamente desde el dispositivo del usuario mediante `CapacitorHttp`, resolviendo restricciones CORS sin recurrir a servidores proxy intermedios.
2. **Cero telemetría**: la aplicación no utiliza Google Analytics, SDKs de publicidad ni herramientas de rastreo.
3. **Almacenamiento local seguro**: las credenciales y datos personales permanecen exclusivamente en el almacenamiento interno cifrado del teléfono del usuario.
### 🤖 Despliegue en Android
---
**Requisito previo**: [Android Studio](https://developer.android.com/studio) instalado, Node.js y NPM.
## 📱 Compilación y Despliegue
### Requisitos Previos
- **Node.js**: v18 o superior.
- **NPM**: v9 o superior.
- **Android**: [Android Studio](https://developer.android.com/studio) con SDK 34+ y JDK 17+.
- **iOS**: macOS con [Xcode 15+](https://developer.apple.com/xcode/) y CocoaPods.
### 🤖 Android
```bash
# 1. Instalar dependencias
npm install
# 2. Compilación del bundle web
# 2. Compilar bundle web
npm run build
# 3. Sincronización con puentes nativos
# 3. Sincronizar puente de Capacitor
npx cap sync android
# 4. Abrir en Android Studio para firmar APK o ejecutar por ADB
# 4. Abrir en Android Studio para generar APK/AAB o ejecutar en dispositivo
npx cap open android
```
### 🍏 Despliegue en iOS (iPhone / iPad)
**Requisito previo**: macOS reciente, [Xcode](https://developer.apple.com/xcode/), Node.js y CocoaPods (`sudo gem install cocoapods`).
### 🍏 iOS
```bash
# 1-2. Instalación y compilación web
# 1. Instalar dependencias y compilar
npm install
npm run build
# 3. Agregar plataforma iOS (solo la primera vez)
# 2. Agregar plataforma iOS (solo la primera vez)
npx cap add ios
# 4. Sincronizar plugins y actualizar Pods
# 3. Sincronizar plugins y actualizar Pods
npx cap sync ios
# 5. Abrir en Xcode
# 4. Abrir en Xcode
npx cap open ios
```
> **Nota para Xcode:** Habilitar permisos explícitos para **Local Notifications** y **Background Fetch** en la pestaña *Signing & Capabilities* de Xcode.
> **Nota para iOS**: Asegurarse de habilitar las capacidades de **Background Modes** (Background Fetch) y **Local Notifications** en la pestaña *Signing & Capabilities* de Xcode.
---
## 🔒 Privacidad y Flujo Cero Intermediarios (Zero-Trust)
## 📄 Licencia
Este proyecto se encuentra bajo la licencia **MIT**. Desarrollado con fines educativos y de soporte a la comunidad estudiantil de la Universidad de la Ciudad.
1. **Credenciales en Moodle Bypass**: La app extrae el PDF de credencial logueándose directamente mediante `CapacitorHttp`, delegando el flujo HTTP al motor nativo del OS. Nunca triangula el DNI a través de un backend tercero.
2. **Resiliencia Anti-Wipes**: Las agresivas políticas *Storage Scoped* de Android 11+ pueden borrar bases de datos SQLite y Preferences al desinstalar. La app mitiga esto exportando automáticamente los favoritos a `aulas_unicaba_datos.csv` en el gestor público de archivos ("Documentos").
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "ar.edu.unicaba.cartelera"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15
versionName "1.2.2"
versionCode 16
versionName "1.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aulas-unicaba",
"version": "1.2.2",
"version": "1.2.3",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
+1 -1
View File
@@ -27,4 +27,4 @@ La aplicación ha sido diseñada bajo una arquitectura de "cliente liviano" con
Esta innovación tecnológica es de carácter informativo. La precisión estricta de asignación de las aulas depende originariamente de la pre-carga impuesta por el cuerpo no-docente en el documento de Spreadsheets. En escenarios contradictorios, la palabra oficial del ala administrativa universitaria primará sobre la respuesta sincronizada en Aulas UNICABA.
---
*Última actualización: 27 de Marzo, 2026 - v1.1.9*
*Última actualización: 28 de Agosto, 2026 - v1.2.3*
@@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-content class="credential-content">
<!-- Loading State -->
<div class="state-container" *ngIf="state === 'loading'">
@@ -54,7 +54,9 @@
<!-- Success Credential State -->
<div class="state-container credential-container" *ngIf="state === 'success'">
<div class="credential-wrapper">
<img [src]="'data:image/png;base64,' + credentialImage" class="credential-img" alt="Credencial UNICABA" />
</div>
<div class="credential-actions">
<ion-button expand="block" color="primary" fill="outline" (click)="generate(true)">
@@ -4,7 +4,7 @@
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
padding: 20px 16px;
}
.loading-text {
@@ -31,7 +31,7 @@
.login-card {
background: var(--ion-card-background, #ffffff);
color: var(--ion-text-color, #000000);
padding: 30px;
padding: 30px 20px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
width: 100%;
@@ -57,23 +57,37 @@
.credential-container {
justify-content: flex-start;
padding-top: 40px;
min-height: auto;
padding: 8px 8px 24px 8px;
width: 100%;
.credential-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.credential-img {
width: 100%;
max-width: 400px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 450px;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
border: 1px solid #efefef;
margin-bottom: 30px;
margin-bottom: 18px;
display: block;
object-fit: contain;
}
.credential-actions {
width: 100%;
max-width: 400px;
max-width: 450px;
padding: 0 4px;
ion-button {
margin-top: 10px;
margin-top: 6px;
font-weight: 600;
letter-spacing: 0.5px;
}
}
}
@@ -94,7 +94,7 @@
<p>No usamos Google Analytics ni SDKs de seguimiento. No hay publicidad. Cero telemetría invasiva.</p>
<div class="footer-info ion-text-center">
<p><small>Aulas UNICABA v1.2.2 • Independiente y Offline-First</small></p>
<p><small>Aulas UNICABA v1.2.3 • Independiente y Offline-First</small></p>
</div>
</div>
</div>
@@ -103,51 +103,223 @@
<div *ngIf="selectedSegment === 'benefits'" class="fade-in">
<div class="logo-container ion-text-center">
<ion-icon name="gift-outline" size="large" color="tertiary"></ion-icon>
<h2>Guía de Beneficios</h2>
<p class="subtitle">Aprovechá tu formación en UNICABA</p>
<h2>Comunidad de Beneficios</h2>
<p class="subtitle">Descuentos exclusivos para la Comunidad UdelaCiudad</p>
</div>
<div class="info-body">
<section class="benefit-section">
<div class="benefit-header">
<ion-icon name="bus" color="primary"></ion-icon>
<h3>1. SOY Estudiante (Transporte)</h3>
</div>
<p>Obtené descuentos en pasajes de micro a todo el país a través del portal de <b>Central de Pasajes</b>.</p>
<div class="steps-box">
<h4>¿Cómo activarlo?</h4>
<ol>
<li>Entra a <b>Perfil</b> y generá tu <b>Certificado de Alumno Regular</b>.</li>
<li>Ingresá a <a href="https://soyestudiante.com.ar/" target="_blank">soyestudiante.com.ar</a>.</li>
<li>Subí el PDF que guardaste en tu teléfono.</li>
<li>¡Listo! Tu DNI quedará habilitado para descuentos automáticos.</li>
</ol>
</div>
</section>
<div class="benefits-container">
<section class="benefit-section">
<div class="benefit-header">
<ion-icon name="logo-google" color="danger"></ion-icon>
<h3>2. Google Students (IA y Nube)</h3>
<!-- ON FIT (DESTACADO) -->
<div class="benefit-card featured">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon gym">
<ion-icon name="barbell-outline"></ion-icon>
</div>
<p>Cargando la <b>Constancia de Alumno Regular</b> con tu cuenta personal te da acceso a herramientas
profesionales.
<div class="title-wrap">
<h3>ON FIT</h3>
<p class="category">Gimnasio & Bienestar</p>
</div>
</div>
<ion-badge color="success" class="discount-badge">25% OFF</ion-badge>
</div>
<p class="benefit-desc">
25% de descuento sobre el valor de la membresía mensual con acceso libre a sus 20 sedes en todo el AMBA.
</p>
<div class="steps-box">
<h4>Beneficios incluidos:</h4>
<div class="inclusion-box">
<h4>¿Qué incluye la membresía?</h4>
<ul>
<li><strong>Google Gemini PRO:</strong> Bonificación por un año de IA avanzada.</li>
<li><strong>Google Drive:</strong> Almacenamiento ampliado (hasta 1TB según vigencia).</li>
<li><strong>Meet Premium:</strong> Reuniones sin límite de tiempo y grabación directa.</li>
<li>Instalaciones con máquinas Technogym de última generación.</li>
<li>Plan de entrenamiento personalizado, actualizado mensualmente.</li>
<li>Clases grupales: funcional, zumba, stretching y más.</li>
<li>Lockers gratuitos en todas las sedes.</li>
</ul>
<p><small>* Para validar, usá tu mail institucional y subí el certificado si se solicita comprobante de
cursada.</small></p>
</div>
</section>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="information-circle-outline"></ion-icon>
<span>Presentá tu Credencial Digital de la app para acreditar pertenencia a la Universidad.</span>
</div>
</div>
<ion-button expand="block" color="success" class="action-btn"
href="https://wa.me/5491158500830?text=Hola!%20Soy%20de%20la%20Universidad%20de%20la%20Ciudad%20y%20deseo%20acceder%20al%20beneficio%20ON%20FIT"
target="_blank">
<ion-icon name="logo-whatsapp" slot="start"></ion-icon>
Consultar por WhatsApp (+54 9 11 5850-0830)
</ion-button>
</div>
<!-- BERESHIT -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon cafe">
<ion-icon name="cafe-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>BERESHIT</h3>
<p class="category">Cafetería</p>
</div>
</div>
<ion-badge color="warning" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Tte. Gral. Perón 893, CABA <em>(a metros de la sede)</em></span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 8 a 20 h • Sábados de 8 a 16 h</span>
</div>
</div>
</div>
<!-- CAFÉ & CHOCOLATE -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon cafe">
<ion-icon name="restaurant-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Café & Chocolate</h3>
<p class="category">Cafetería & Delicatessen</p>
</div>
</div>
<ion-badge color="warning" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Bartolomé Mitre 807, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 8 a 19 h • Sábados de 8 a 14 h</span>
</div>
</div>
</div>
<!-- AL BUEN TALLARÍN DE PABLO -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon pasta">
<ion-icon name="pizza-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Al Buen Tallarín de Pablo</h3>
<p class="category">Fábrica de Pastas</p>
</div>
</div>
<ion-badge color="danger" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Tucumán 1596, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Mar a Vie de 8 a 19 h • Sáb y Dom de 9:30 a 14:30 h</span>
</div>
</div>
</div>
<!-- ATLAS COMERCIAL -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon libreria">
<ion-icon name="book-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Atlas Comercial</h3>
<p class="category">Librería & Papelería</p>
</div>
</div>
<ion-badge color="primary" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Bartolomé Mitre 839, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 9 a 18 h</span>
</div>
</div>
</div>
<!-- FLUIRE STUDIO -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon pilates">
<ion-icon name="body-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Fluire Studio</h3>
<p class="category">Pilates & Salud</p>
</div>
</div>
<ion-badge color="tertiary" class="discount-badge">Hasta 20% OFF</ion-badge>
</div>
<p class="benefit-desc">
10% de descuento abonando 1 mes de servicio y 20% de descuento abonando 4 meses de servicio.
</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Esmeralda 135, piso 4 B, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="mail-outline"></ion-icon>
<span>Contacto: fluire.studio.ba@gmail.com</span>
</div>
</div>
<ion-button expand="block" fill="outline" color="tertiary" class="action-btn"
href="mailto:fluire.studio.ba@gmail.com?subject=Consulta%20Beneficio%20Universidad%20de%20la%20Ciudad">
<ion-icon name="mail-outline" slot="start"></ion-icon>
Contactar por Email
</ion-button>
</div>
<!-- SOY ESTUDIANTE -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon travel">
<ion-icon name="bus-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>SOY Estudiante</h3>
<p class="category">Transporte de Larga Distancia</p>
</div>
</div>
<ion-badge color="secondary" class="discount-badge">Descuento Nacional</ion-badge>
</div>
<p class="benefit-desc">
Obtené descuentos en pasajes de micro a todo el país a través del portal de Central de Pasajes.
</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="checkmark-circle-outline"></ion-icon>
<span>Entrá a <b>Perfil</b>, generá tu Certificado de Alumno Regular y subilo a <a href="https://soyestudiante.com.ar/" target="_blank">soyestudiante.com.ar</a>.</span>
</div>
</div>
</div>
</div>
<div class="footer-info ion-text-center">
<p><small>Los beneficios son gestionados por terceros y sujetos a sus términos y condiciones.</small></p>
</div>
<p><small>Presentá tu credencial digital de Aulas UNICABA en los comercios adheridos para acceder a los descuentos.</small></p>
</div>
</div>
@@ -76,6 +76,153 @@ ion-segment {
}
}
/* --- ESTILOS DE BENEFICIOS --- */
.benefits-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 25px;
}
.benefit-card {
background: white;
border-radius: 18px;
padding: 18px;
box-shadow: 0 4px 15px rgba(0,0,0,0.06);
border: 1px solid rgba(0,0,0,0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&.featured {
border: 1.5px solid var(--ion-color-primary);
background: linear-gradient(180deg, #ffffff 0%, rgba(var(--ion-color-primary-rgb), 0.02) 100%);
box-shadow: 0 6px 20px rgba(var(--ion-color-primary-rgb), 0.12);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.brand-info {
display: flex;
align-items: center;
gap: 12px;
.brand-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(var(--ion-color-primary-rgb), 0.1);
color: var(--ion-color-primary);
font-size: 24px;
flex-shrink: 0;
&.cafe { background: #fff3e0; color: #e65100; }
&.gym { background: #e8f5e9; color: #2e7d32; }
&.pilates { background: #ede7f6; color: #512da8; }
&.pasta { background: #fbe9e7; color: #d84315; }
&.libreria { background: #e1f5fe; color: #0288d1; }
&.travel { background: #e3f2fd; color: #1565c0; }
}
.title-wrap {
h3 {
margin: 0;
font-size: 1.05rem;
font-weight: 800;
color: var(--ion-color-dark);
}
.category {
margin: 2px 0 0;
font-size: 0.8rem;
color: var(--ion-color-medium);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
.discount-badge {
font-size: 0.78rem;
font-weight: 800;
padding: 5px 10px;
border-radius: 20px;
letter-spacing: 0.3px;
}
}
.benefit-desc {
font-size: 0.92rem;
color: var(--ion-color-dark-shade);
margin: 0 0 12px;
line-height: 1.45;
}
.meta-grid {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
margin-bottom: 12px;
.meta-row {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 0.84rem;
color: var(--ion-color-medium-shade);
line-height: 1.4;
ion-icon {
font-size: 16px;
margin-top: 2px;
color: var(--ion-color-primary);
flex-shrink: 0;
}
}
}
.inclusion-box {
background: #f8fafc;
border-radius: 12px;
padding: 12px 14px;
margin: 10px 0;
border-left: 3px solid var(--ion-color-success);
h4 {
margin: 0 0 6px;
font-size: 0.85rem;
font-weight: 700;
color: var(--ion-color-dark);
}
ul {
margin: 0;
padding-left: 18px;
li {
font-size: 0.82rem;
margin-bottom: 4px;
color: #475569;
&:last-child { margin-bottom: 0; }
}
}
}
.action-btn {
--border-radius: 12px;
font-weight: 700;
font-size: 0.88rem;
margin-top: 6px;
letter-spacing: 0.3px;
}
}
.footer-info {
margin-top: 30px;
opacity: 0.6;
+2 -2
View File
@@ -103,8 +103,8 @@
<!-- Icono condicional: Hamburguesa en favoritos, Estrella en cartelera -->
<ion-icon class="menu-icon"
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item.codigo) ? 'star' : 'star-outline')"
[color]="(viewMode === 'todas' && isFavorito(item.codigo)) ? 'warning' : 'medium'"
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item) ? 'star' : 'star-outline')"
[color]="(viewMode === 'todas' && isFavorito(item)) ? 'warning' : 'medium'"
(click)="openMateriaActions(item)"></ion-icon>
</div>
</div>
+135 -22
View File
@@ -108,7 +108,7 @@ export class HomePage implements OnInit {
this.highlightedCodigo = params['highlight'] || null;
if (this.highlightedCodigo) {
// Aseguramos que el highlight sea visible (si no es favorito, ir a 'todas')
if (this.viewMode === 'favoritos' && !this.favoritos.includes(this.highlightedCodigo)) {
if (this.viewMode === 'favoritos' && !this.isFavorito(this.highlightedCodigo)) {
this.viewMode = 'todas';
}
}
@@ -223,7 +223,7 @@ export class HomePage implements OnInit {
this.errorMsg = '';
// Programar recordatorios de clase 30 min antes
const favItems = this.horarios.filter(h => this.favoritos.includes(h.codigo));
const favItems = this.horarios.filter(h => this.isFavorito(h));
this.dataService.scheduleClassReminders(favItems);
// Auto-reparación/Actualización del Respaldo
@@ -262,7 +262,7 @@ export class HomePage implements OnInit {
let baseList = this.horarios;
if (this.viewMode === 'favoritos') {
baseList = baseList.filter(h => this.favoritos.includes(h.codigo));
baseList = baseList.filter(h => this.isFavorito(h));
this.actualizarProximaClase();
} else {
this.proximaClase = null;
@@ -293,7 +293,7 @@ export class HomePage implements OnInit {
let candidata: ProximaClaseInfo | null = null;
// 1. Buscar si hay alguna clase en curso AHORA
const favsHoy = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === hoyIdx);
const favsHoy = this.horarios.filter(h => this.isFavorito(h) && this.DIAS_DOW[h.dia || ''] === hoyIdx);
for (const item of favsHoy) {
const parts = (item.horario || '').split('-');
@@ -331,7 +331,7 @@ export class HomePage implements OnInit {
// Si no encontramos hoy, buscamos mañana
if (!candidata) {
const mananaIdx = (hoyIdx + 1) % 7;
const favsManana = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === mananaIdx);
const favsManana = this.horarios.filter(h => this.isFavorito(h) && this.DIAS_DOW[h.dia || ''] === mananaIdx);
for (const item of favsManana) {
const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/);
@@ -456,12 +456,36 @@ export class HomePage implements OnInit {
this.filterHorarios();
}
isFavorito(codigo: string): boolean {
return this.favoritos.includes(codigo);
getItemKey(item: ClaseHorario): string {
if (!item) return '';
return `${item.codigo}::${item.asignatura.trim()}`;
}
isFavorito(item: ClaseHorario | string): boolean {
if (!item) return false;
if (typeof item === 'string') {
return this.favoritos.some(fav => fav === item || fav.startsWith(`${item}::`));
}
const itemKey = this.getItemKey(item);
// 1. Coincidencia directa por clave específica (ej: "ASIG00127::Programación de Vanguardia - Equipo 1")
if (this.favoritos.includes(itemKey)) return true;
// 2. Coincidencia por código base (ej: "ASIG00125")
if (this.favoritos.includes(item.codigo)) {
// Si en favoritos existe una clave más específica para este código (ej: "ASIG00127::..."),
// el código plano NO debe marcar a las demás variantes
const hasSpecificForCode = this.favoritos.some(fav => fav.startsWith(`${item.codigo}::`));
return !hasSpecificForCode;
}
return false;
}
async toggleFavorito(item: ClaseHorario) {
const isFav = this.isFavorito(item.codigo);
const isFav = this.isFavorito(item);
const itemKey = this.getItemKey(item);
const alert = await this.alertCtrl.create({
header: isFav ? 'Quitar de Favoritos' : 'Agregar a Favoritos',
@@ -477,10 +501,23 @@ export class HomePage implements OnInit {
text: 'Aceptar',
handler: async () => {
if (isFav) {
this.favoritos = this.favoritos.filter(id => id !== item.codigo);
// Remover tanto la clave específica como el código genérico si existía
this.favoritos = this.favoritos.filter(id => id !== itemKey && id !== item.codigo);
} else {
// Verificar si hay múltiples filas con el mismo código en la cartelera (ej. Equipo 1 y Equipo 2)
const sameCodeCount = this.horarios.filter(h => h.codigo === item.codigo).length;
if (sameCodeCount > 1) {
// Hay múltiples equipos/comisiones: guardar clave específica para esta materia y equipo
if (!this.favoritos.includes(itemKey)) {
this.favoritos.push(itemKey);
}
} else {
// Código único: guardar código base
if (!this.favoritos.includes(item.codigo) && !this.favoritos.includes(itemKey)) {
this.favoritos.push(item.codigo);
}
}
}
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
this.guardarProgreso();
this.filterHorarios();
@@ -572,7 +609,7 @@ export class HomePage implements OnInit {
async openMateriaActions(item: ClaseHorario) {
// Si estamos en cartelera general y NO es favorito, lo agregamos directamente
if (this.viewMode === 'todas' && !this.isFavorito(item.codigo)) {
if (this.viewMode === 'todas' && !this.isFavorito(item)) {
this.toggleFavorito(item);
return;
}
@@ -1073,32 +1110,56 @@ export class HomePage implements OnInit {
// --- FAVORITOS: Sincronización Inteligente ---
if (this.horarios && this.horarios.length > 0) {
// Prioridad 1: Por Código Exacto (Desde la página de Inscripción / Cursada)
// Prioridad 1: Por Código e Identificación Precisa de Equipo/Comisión (Desde Inscripciones Activas de SIU)
if (inscripcionesSiu.length > 0) {
for (const insc of inscripcionesSiu) {
// Buscamos en nuestra cartelera si existe este código ASIG
const matchHorarios = this.horarios.filter(h => h.codigo === insc.codigo);
for (const h of matchHorarios) {
if (!this.favoritos.includes(h.codigo)) {
this.favoritos = [...this.favoritos, h.codigo];
const matchedHorario = this.encontrarHorarioParaInscripcionSiu(insc, this.horarios);
if (matchedHorario) {
const itemKey = this.getItemKey(matchedHorario);
const sameCodeItems = this.horarios.filter(h => h.codigo === matchedHorario.codigo);
if (sameCodeItems.length > 1) {
// Si hay múltiples equipos/comisiones (ej. Equipo 1 y Equipo 2):
// Removemos el código genérico u otros equipos de este mismo código para dejar solo el correcto
const oldFavsCount = this.favoritos.length;
this.favoritos = this.favoritos.filter(f => f !== matchedHorario.codigo && !sameCodeItems.some(h => f === this.getItemKey(h) && f !== itemKey));
if (!this.favoritos.includes(itemKey)) {
this.favoritos.push(itemKey);
changed = true;
} else if (this.favoritos.length !== oldFavsCount) {
changed = true;
}
} else {
// Código único
if (!this.isFavorito(matchedHorario)) {
this.favoritos.push(matchedHorario.codigo);
changed = true;
}
}
}
}
}
// Prioridad 2: Por Nombre (Caso de fallback o si no hay códigos en la cartelera todavía)
// Prioridad 2: Por Nombre (Fallback si la materia no vino en inscripciones activas)
if (this.siuMaterias && this.siuMaterias.length > 0) {
const enCursoEnSiu = this.siuMaterias.filter(m => this.determinarEstadoSiu(m.nota, m.origen) === 'encurso');
for (const siuMat of enCursoEnSiu) {
// Si ya la tenemos por código, omitimos el sync por nombre para evitar duplicados erroneos
const yaEnFavs = this.horarios.some(h => this.favoritos.includes(h.codigo) && this.matcheaSiu(siuMat.nombre, h.asignatura));
const yaEnFavs = this.horarios.some(h => this.isFavorito(h) && this.matcheaSiu(siuMat.nombre, h.asignatura));
if (yaEnFavs) continue;
const matchHorarios = this.horarios.filter(h => this.matcheaSiu(siuMat.nombre, h.asignatura));
for (const h of matchHorarios) {
if (!this.favoritos.includes(h.codigo)) {
this.favoritos = [...this.favoritos, h.codigo];
if (matchHorarios.length === 1) {
const h = matchHorarios[0];
if (!this.isFavorito(h)) {
this.favoritos.push(h.codigo);
changed = true;
}
} else if (matchHorarios.length > 1) {
const h = matchHorarios[0];
const itemKey = this.getItemKey(h);
if (!this.isFavorito(h)) {
this.favoritos.push(itemKey);
changed = true;
}
}
@@ -1115,6 +1176,58 @@ export class HomePage implements OnInit {
if (changed) this.guardarProgreso();
}
/**
* Resuelve la discrepancia entre el código con sufijo de SIU (ej: ASIG00127-E1-TD)
* y los registros de la Cartelera (código ASIG00127 con nombre 'Programación de Vanguardia - Equipo 1')
*/
encontrarHorarioParaInscripcionSiu(insc: { nombre: string, codigo: string }, horarios: ClaseHorario[]): ClaseHorario | null {
if (!insc || !horarios || horarios.length === 0) return null;
// 1. Extraer código base ASIG (ej: de "ASIG00127-E1-TD" extrae "ASIG00127")
const asigMatch = insc.codigo.match(/ASIG\d+/i);
const asigBase = asigMatch ? asigMatch[0].toUpperCase() : '';
// 2. Extraer Equipo / Comisión del código SIU (ej: de "ASIG00127-E1-TD" extrae "1")
const siuTeamMatch = insc.codigo.match(/[-_](?:E|EQ|EQUIPO|C|COM|COMISION)\s*([0-9A-Za-z]+)/i);
const siuTeam = siuTeamMatch ? siuTeamMatch[1].toUpperCase() : null;
// 3. Filtrar candidatos por código ASIG base o por coincidencia de nombre
let candidatos = horarios.filter(h => {
const codeMatch = asigBase && h.codigo.toUpperCase() === asigBase;
const nameMatch = this.matcheaSiu(insc.nombre, h.asignatura);
return codeMatch || nameMatch;
});
if (candidatos.length === 0) return null;
if (candidatos.length === 1) return candidatos[0];
// 4. Si hay múltiples candidatos (ej. Equipo 1 y Equipo 2):
if (siuTeam) {
// Buscar el candidato cuyo nombre de asignatura coincida con el Equipo/Comisión (ej: "Equipo 1", "Eq 1", "Comisión 1")
const candidatoEquipo = candidatos.find(h => {
const sheetTeamMatch = h.asignatura.match(/(?:Equipo|Eq\.?|Comisi[oó]n|Com\.?)\s*([0-9A-Za-z]+)/i);
const sheetTeam = sheetTeamMatch ? sheetTeamMatch[1].toUpperCase() : null;
return sheetTeam === siuTeam;
});
if (candidatoEquipo) return candidatoEquipo;
}
// 5. Fallback por carrera si está disponible (ej: TD -> TEC. DIG.)
if (insc.codigo.toUpperCase().includes('-TD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('TEC') || h.carrera.toUpperCase().includes('DIG'));
if (candidatoCarrera) return candidatoCarrera;
} else if (insc.codigo.toUpperCase().includes('-CD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('DATOS'));
if (candidatoCarrera) return candidatoCarrera;
} else if (insc.codigo.toUpperCase().includes('-VJ') || insc.codigo.toUpperCase().includes('-VD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('VIDEO'));
if (candidatoCarrera) return candidatoCarrera;
}
return candidatos[0];
}
autoAsignarOptativasSiu(): boolean {
if (!this.siuMaterias) return false;
let mappedAlgo = false;
+1 -1
View File
@@ -316,7 +316,7 @@ export class CredencialService {
const page = await pdf.getPage(1);
const scale = 2.0;
const scale = 3.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
+12 -1
View File
@@ -185,11 +185,22 @@ export class DataService {
return (hash % 2_000_000) + 100_000;
}
private isItemInFavorites(item: ClaseHorario, favoritos: string[]): boolean {
const itemKey = `${item.codigo}::${item.asignatura.trim()}`;
if (favoritos.includes(itemKey)) return true;
if (favoritos.includes(item.codigo)) {
const hasSpecific = favoritos.some(f => f.startsWith(`${item.codigo}::`));
return !hasSpecific;
}
return false;
}
private async checkRoomChanges(oldData: ClaseHorario[], newData: ClaseHorario[], favoritos: string[]) {
for (const item of newData) {
if (favoritos.includes(item.codigo)) {
if (this.isItemInFavorites(item, favoritos)) {
const oldItem = oldData.find(o =>
o.codigo === item.codigo &&
o.asignatura === item.asignatura &&
o.dia === item.dia &&
o.horario === item.horario
);
+52 -11
View File
@@ -7,7 +7,7 @@ import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class SiuService {
constructor(private authService: AuthService) {}
private readonly BASE_URL = 'https://siuuniciu.eastus2.cloudapp.azure.com/caba';
private readonly BASE_URL = 'https://autogestion.udelaciudad.edu.ar/caba';
async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise<any[]> {
try {
@@ -49,7 +49,27 @@ export class SiuService {
const rawHtml = resCursada.data;
// Buscamos el bloque on_arrival que contiene "info_materia" (donde están las inscripciones)
// 1. Extraer directamente de "notificaciones" donde SIU provee el array JSON "asignaciones"
const regexNotif = /on_arrival\((\{.*?"id":"notificaciones".*?\})\);/s;
const matchNotif = rawHtml.match(regexNotif);
if (matchNotif && matchNotif[1]) {
try {
const jsonNotif = JSON.parse(matchNotif[1]);
const asignaciones = jsonNotif.info?.asignaciones;
if (Array.isArray(asignaciones) && asignaciones.length > 0) {
return asignaciones.map((asig: any) => ({
nombre: asig.NOMBRE_MATERIA || asig.MATERIA,
codigo: asig.NOMBRE || asig.MATERIA, // ej: "ASIG00127-E1-TD"
comision: asig.COMISION,
dia: asig.DIA_SEMANA
}));
}
} catch (e) {
console.warn('No se pudieron leer asignaciones estructuradas:', e);
}
}
// 2. Fallback: extraer del bloque HTML "info_materia" (tabla .js-mis-inscripciones)
const regex = /on_arrival\((\{.*?"id":"info_materia".*?\})\);/s;
const match = rawHtml.match(regex);
@@ -59,7 +79,7 @@ export class SiuService {
return this.parseInscripcionesActivasHtml(contentHtml);
}
return []; // Quizás no tiene inscripciones activas
return [];
} catch (error) {
console.error('Error SIU Active Inscriptions:', error);
return [];
@@ -254,20 +274,41 @@ export class SiuService {
private parseInscripcionesActivasHtml(html: string): any[] {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Buscamos los elementos que contienen los nombres de las materias (según el HTML del usuario)
const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td'));
// Buscamos los elementos que contienen las inscripciones activas
const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td, .inscripciones-lista td, .cuadro_materias td, table.tabla_cursada td'));
return rows.map(td => {
const results: any[] = [];
for (const td of rows) {
const text = td.textContent?.trim() || '';
// Formato esperado: "Nombre Materia - ASIG00125"
if (!text || text.length < 5) continue;
// Caso 1: "Nombre Materia - ASIG00127-E1-TD"
if (text.includes(' - ')) {
const parts = text.split(' - ');
if (parts.length >= 2) {
const codigo = parts[parts.length - 1].trim();
const nombre = parts.slice(0, -1).join(' - ').trim();
return { nombre, codigo };
if (codigo.toUpperCase().includes('ASIG') || codigo.length >= 4) {
results.push({ nombre, codigo });
continue;
}
return null;
}).filter(item => item !== null);
}
// Caso 2: "Nombre Materia (ASIG00127-E1-TD)"
const matchParen = text.match(/^(.*?)\s*\((ASIG[^\)]+)\)/i);
if (matchParen) {
results.push({ nombre: matchParen[1].trim(), codigo: matchParen[2].trim() });
continue;
}
// Caso 3: contiene código ASIG embebido
const matchAsig = text.match(/(ASIG[A-Z0-9\-_]+)/i);
if (matchAsig) {
const codigo = matchAsig[1].trim();
const nombre = text.replace(codigo, '').replace(/[-–()]/g, '').trim();
results.push({ nombre, codigo });
}
}
return results;
}
private parseHtmlSubjects(html: string): any[] {