v1.2.3: Fix SIU SSL, matching de comisiones/equipos, credencial QR ampliada y actualizacion de beneficios
This commit is contained in:
@@ -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).
|
[](package.json)
|
||||||
|
[](https://angular.io/)
|
||||||
|
[](https://ionicframework.com/)
|
||||||
|
[](https://capacitorjs.com/)
|
||||||
|
[](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
|
### 📋 Cartelera de Horarios y Aulas
|
||||||
- Visualización completa de la cartelera oficial de aulas organizada por día de la semana (Lunes a Sábado).
|
- **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 cachea localmente al primer acceso y se muestra instantáneamente aunque no haya conexión.
|
- **Modo offline-first**: la cartelera se almacena en caché local y se encuentra disponible instantáneamente sin requerir conexión a internet.
|
||||||
- **Sincronización automática** en background (Background Fetch) delegada al sistema operativo.
|
- **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.
|
||||||
- **Búsqueda fuzzy** tolerante a errores tipográficos: busca por nombre de materia, código, aula o carrera.
|
- **Segmentos de visualización**: alternancia rápida entre *Favoritos*, *Cartelera completa* y *Mi Carrera*.
|
||||||
- **Filtro por vista**: alternás entre "Favoritos" y "Cartelera completa" con un segmento.
|
- **Acceso directo a Aulas Virtuales**: botón integrado para ingresar a clases remotas en Zoom o Google Meet.
|
||||||
- Acceso directo a clases virtuales (Zoom / Meet) desde el botón de cada materia.
|
- **Compartir horario**: exportación nativa de los datos de cursada a WhatsApp, Telegram y otras aplicaciones del sistema (`@capacitor/share`).
|
||||||
- Función de compartir el horario de una materia por WhatsApp, Telegram, y cualquier app nativa.
|
- **Sincronización en background**: actualización periódica y desatendida delegada al sistema operativo mediante Background Fetch.
|
||||||
|
|
||||||
### ⭐ 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ 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/) |
|
| `@capacitor/filesystem` | Lectura y escritura de respaldos CSV en la carpeta pública "Documentos". |
|
||||||
| UI Components | [Ionic Framework v8](https://ionicframework.com/) |
|
| `@capawesome/capacitor-file-picker` | Selector de archivos para importación manual de respaldos. |
|
||||||
| Puente Nativo | [Capacitor v8](https://capacitorjs.com/) |
|
| `@capacitor/local-notifications` | Notificaciones locales de cambios de aula y recordatorios pre-clase. |
|
||||||
| Renderizado PDF | `pdfjs-dist` |
|
| `@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. |
|
||||||
### Plugins de Capacitor utilizados
|
| `@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. |
|
||||||
| Plugin | Uso |
|
| `@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/filesystem` | Escritura y lectura del CSV de respaldo en "Documentos" |
|
| `@capacitor/share` | Diálogo nativo para compartir horarios por mensajería. |
|
||||||
| `@capawesome/capacitor-file-picker` | Importación manual de CSV desde el explorador de archivos |
|
| `@capacitor/network` | Monitorización del estado de conectividad a internet. |
|
||||||
| `@capacitor/local-notifications` | Avisos de cambio de aula y recordatorios pre-clase |
|
| `@capacitor/clipboard` | Utilidades de portapapeles. |
|
||||||
| `@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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📱 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
|
```bash
|
||||||
# 1. Instalar dependencias
|
# 1. Instalar dependencias
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 2. Compilación del bundle web
|
# 2. Compilar bundle web
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 3. Sincronización con puentes nativos
|
# 3. Sincronizar puente de Capacitor
|
||||||
npx cap sync android
|
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
|
npx cap open android
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🍏 Despliegue en iOS (iPhone / iPad)
|
### 🍏 iOS
|
||||||
|
|
||||||
**Requisito previo**: macOS reciente, [Xcode](https://developer.apple.com/xcode/), Node.js y CocoaPods (`sudo gem install cocoapods`).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1-2. Instalación y compilación web
|
# 1. Instalar dependencias y compilar
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 3. Agregar plataforma iOS (solo la primera vez)
|
# 2. Agregar plataforma iOS (solo la primera vez)
|
||||||
npx cap add ios
|
npx cap add ios
|
||||||
|
|
||||||
# 4. Sincronizar plugins y actualizar Pods
|
# 3. Sincronizar plugins y actualizar Pods
|
||||||
npx cap sync ios
|
npx cap sync ios
|
||||||
|
|
||||||
# 5. Abrir en Xcode
|
# 4. Abrir en Xcode
|
||||||
npx cap open ios
|
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").
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "ar.edu.unicaba.cartelera"
|
applicationId "ar.edu.unicaba.cartelera"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 15
|
versionCode 16
|
||||||
versionName "1.2.2"
|
versionName "1.2.3"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aulas-unicaba",
|
"name": "aulas-unicaba",
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"author": "Ionic Framework",
|
"author": "Ionic Framework",
|
||||||
"homepage": "https://ionicframework.com/",
|
"homepage": "https://ionicframework.com/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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.
|
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-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="credential-content">
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div class="state-container" *ngIf="state === 'loading'">
|
<div class="state-container" *ngIf="state === 'loading'">
|
||||||
@@ -54,7 +54,9 @@
|
|||||||
|
|
||||||
<!-- Success Credential State -->
|
<!-- Success Credential State -->
|
||||||
<div class="state-container credential-container" *ngIf="state === 'success'">
|
<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" />
|
<img [src]="'data:image/png;base64,' + credentialImage" class="credential-img" alt="Credencial UNICABA" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="credential-actions">
|
<div class="credential-actions">
|
||||||
<ion-button expand="block" color="primary" fill="outline" (click)="generate(true)">
|
<ion-button expand="block" color="primary" fill="outline" (click)="generate(true)">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 80vh;
|
min-height: 80vh;
|
||||||
padding: 20px;
|
padding: 20px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
.login-card {
|
.login-card {
|
||||||
background: var(--ion-card-background, #ffffff);
|
background: var(--ion-card-background, #ffffff);
|
||||||
color: var(--ion-text-color, #000000);
|
color: var(--ion-text-color, #000000);
|
||||||
padding: 30px;
|
padding: 30px 20px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -57,23 +57,37 @@
|
|||||||
|
|
||||||
.credential-container {
|
.credential-container {
|
||||||
justify-content: flex-start;
|
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 {
|
.credential-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 450px;
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
border: 1px solid #efefef;
|
border: 1px solid #efefef;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 18px;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credential-actions {
|
.credential-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 450px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
ion-button {
|
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>
|
<p>No usamos Google Analytics ni SDKs de seguimiento. No hay publicidad. Cero telemetría invasiva.</p>
|
||||||
|
|
||||||
<div class="footer-info ion-text-center">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,51 +103,223 @@
|
|||||||
<div *ngIf="selectedSegment === 'benefits'" class="fade-in">
|
<div *ngIf="selectedSegment === 'benefits'" class="fade-in">
|
||||||
<div class="logo-container ion-text-center">
|
<div class="logo-container ion-text-center">
|
||||||
<ion-icon name="gift-outline" size="large" color="tertiary"></ion-icon>
|
<ion-icon name="gift-outline" size="large" color="tertiary"></ion-icon>
|
||||||
<h2>Guía de Beneficios</h2>
|
<h2>Comunidad de Beneficios</h2>
|
||||||
<p class="subtitle">Aprovechá tu formación en UNICABA</p>
|
<p class="subtitle">Descuentos exclusivos para la Comunidad UdelaCiudad</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-body">
|
<div class="benefits-container">
|
||||||
<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>
|
|
||||||
|
|
||||||
<section class="benefit-section">
|
<!-- ON FIT (DESTACADO) -->
|
||||||
<div class="benefit-header">
|
<div class="benefit-card featured">
|
||||||
<ion-icon name="logo-google" color="danger"></ion-icon>
|
<div class="card-top">
|
||||||
<h3>2. Google Students (IA y Nube)</h3>
|
<div class="brand-info">
|
||||||
|
<div class="brand-icon gym">
|
||||||
|
<ion-icon name="barbell-outline"></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
<p>Cargando la <b>Constancia de Alumno Regular</b> con tu cuenta personal te da acceso a herramientas
|
<div class="title-wrap">
|
||||||
profesionales.
|
<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>
|
</p>
|
||||||
<div class="steps-box">
|
<div class="inclusion-box">
|
||||||
<h4>Beneficios incluidos:</h4>
|
<h4>¿Qué incluye la membresía?</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Google Gemini PRO:</strong> Bonificación por un año de IA avanzada.</li>
|
<li>Instalaciones con máquinas Technogym de última generación.</li>
|
||||||
<li><strong>Google Drive:</strong> Almacenamiento ampliado (hasta 1TB según vigencia).</li>
|
<li>Plan de entrenamiento personalizado, actualizado mensualmente.</li>
|
||||||
<li><strong>Meet Premium:</strong> Reuniones sin límite de tiempo y grabación directa.</li>
|
<li>Clases grupales: funcional, zumba, stretching y más.</li>
|
||||||
|
<li>Lockers gratuitos en todas las sedes.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><small>* Para validar, usá tu mail institucional y subí el certificado si se solicita comprobante de
|
|
||||||
cursada.</small></p>
|
|
||||||
</div>
|
</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">
|
<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>
|
<p><small>Presentá tu credencial digital de Aulas UNICABA en los comercios adheridos para acceder a los descuentos.</small></p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
.footer-info {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|||||||
@@ -103,8 +103,8 @@
|
|||||||
|
|
||||||
<!-- Icono condicional: Hamburguesa en favoritos, Estrella en cartelera -->
|
<!-- Icono condicional: Hamburguesa en favoritos, Estrella en cartelera -->
|
||||||
<ion-icon class="menu-icon"
|
<ion-icon class="menu-icon"
|
||||||
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item.codigo) ? 'star' : 'star-outline')"
|
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item) ? 'star' : 'star-outline')"
|
||||||
[color]="(viewMode === 'todas' && isFavorito(item.codigo)) ? 'warning' : 'medium'"
|
[color]="(viewMode === 'todas' && isFavorito(item)) ? 'warning' : 'medium'"
|
||||||
(click)="openMateriaActions(item)"></ion-icon>
|
(click)="openMateriaActions(item)"></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+135
-22
@@ -108,7 +108,7 @@ export class HomePage implements OnInit {
|
|||||||
this.highlightedCodigo = params['highlight'] || null;
|
this.highlightedCodigo = params['highlight'] || null;
|
||||||
if (this.highlightedCodigo) {
|
if (this.highlightedCodigo) {
|
||||||
// Aseguramos que el highlight sea visible (si no es favorito, ir a 'todas')
|
// 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';
|
this.viewMode = 'todas';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ export class HomePage implements OnInit {
|
|||||||
this.errorMsg = '';
|
this.errorMsg = '';
|
||||||
|
|
||||||
// Programar recordatorios de clase 30 min antes
|
// 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);
|
this.dataService.scheduleClassReminders(favItems);
|
||||||
|
|
||||||
// Auto-reparación/Actualización del Respaldo
|
// Auto-reparación/Actualización del Respaldo
|
||||||
@@ -262,7 +262,7 @@ export class HomePage implements OnInit {
|
|||||||
let baseList = this.horarios;
|
let baseList = this.horarios;
|
||||||
|
|
||||||
if (this.viewMode === 'favoritos') {
|
if (this.viewMode === 'favoritos') {
|
||||||
baseList = baseList.filter(h => this.favoritos.includes(h.codigo));
|
baseList = baseList.filter(h => this.isFavorito(h));
|
||||||
this.actualizarProximaClase();
|
this.actualizarProximaClase();
|
||||||
} else {
|
} else {
|
||||||
this.proximaClase = null;
|
this.proximaClase = null;
|
||||||
@@ -293,7 +293,7 @@ export class HomePage implements OnInit {
|
|||||||
let candidata: ProximaClaseInfo | null = null;
|
let candidata: ProximaClaseInfo | null = null;
|
||||||
|
|
||||||
// 1. Buscar si hay alguna clase en curso AHORA
|
// 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) {
|
for (const item of favsHoy) {
|
||||||
const parts = (item.horario || '').split('-');
|
const parts = (item.horario || '').split('-');
|
||||||
@@ -331,7 +331,7 @@ export class HomePage implements OnInit {
|
|||||||
// Si no encontramos hoy, buscamos mañana
|
// Si no encontramos hoy, buscamos mañana
|
||||||
if (!candidata) {
|
if (!candidata) {
|
||||||
const mananaIdx = (hoyIdx + 1) % 7;
|
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) {
|
for (const item of favsManana) {
|
||||||
const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/);
|
const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/);
|
||||||
@@ -456,12 +456,36 @@ export class HomePage implements OnInit {
|
|||||||
this.filterHorarios();
|
this.filterHorarios();
|
||||||
}
|
}
|
||||||
|
|
||||||
isFavorito(codigo: string): boolean {
|
getItemKey(item: ClaseHorario): string {
|
||||||
return this.favoritos.includes(codigo);
|
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) {
|
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({
|
const alert = await this.alertCtrl.create({
|
||||||
header: isFav ? 'Quitar de Favoritos' : 'Agregar a Favoritos',
|
header: isFav ? 'Quitar de Favoritos' : 'Agregar a Favoritos',
|
||||||
@@ -477,10 +501,23 @@ export class HomePage implements OnInit {
|
|||||||
text: 'Aceptar',
|
text: 'Aceptar',
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
if (isFav) {
|
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 {
|
} 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);
|
this.favoritos.push(item.codigo);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
|
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
|
||||||
this.guardarProgreso();
|
this.guardarProgreso();
|
||||||
this.filterHorarios();
|
this.filterHorarios();
|
||||||
@@ -572,7 +609,7 @@ export class HomePage implements OnInit {
|
|||||||
|
|
||||||
async openMateriaActions(item: ClaseHorario) {
|
async openMateriaActions(item: ClaseHorario) {
|
||||||
// Si estamos en cartelera general y NO es favorito, lo agregamos directamente
|
// 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);
|
this.toggleFavorito(item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1073,32 +1110,56 @@ export class HomePage implements OnInit {
|
|||||||
|
|
||||||
// --- FAVORITOS: Sincronización Inteligente ---
|
// --- FAVORITOS: Sincronización Inteligente ---
|
||||||
if (this.horarios && this.horarios.length > 0) {
|
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) {
|
if (inscripcionesSiu.length > 0) {
|
||||||
for (const insc of inscripcionesSiu) {
|
for (const insc of inscripcionesSiu) {
|
||||||
// Buscamos en nuestra cartelera si existe este código ASIG
|
const matchedHorario = this.encontrarHorarioParaInscripcionSiu(insc, this.horarios);
|
||||||
const matchHorarios = this.horarios.filter(h => h.codigo === insc.codigo);
|
if (matchedHorario) {
|
||||||
for (const h of matchHorarios) {
|
const itemKey = this.getItemKey(matchedHorario);
|
||||||
if (!this.favoritos.includes(h.codigo)) {
|
const sameCodeItems = this.horarios.filter(h => h.codigo === matchedHorario.codigo);
|
||||||
this.favoritos = [...this.favoritos, h.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;
|
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) {
|
if (this.siuMaterias && this.siuMaterias.length > 0) {
|
||||||
const enCursoEnSiu = this.siuMaterias.filter(m => this.determinarEstadoSiu(m.nota, m.origen) === 'encurso');
|
const enCursoEnSiu = this.siuMaterias.filter(m => this.determinarEstadoSiu(m.nota, m.origen) === 'encurso');
|
||||||
for (const siuMat of enCursoEnSiu) {
|
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.isFavorito(h) && this.matcheaSiu(siuMat.nombre, h.asignatura));
|
||||||
const yaEnFavs = this.horarios.some(h => this.favoritos.includes(h.codigo) && this.matcheaSiu(siuMat.nombre, h.asignatura));
|
|
||||||
if (yaEnFavs) continue;
|
if (yaEnFavs) continue;
|
||||||
|
|
||||||
const matchHorarios = this.horarios.filter(h => this.matcheaSiu(siuMat.nombre, h.asignatura));
|
const matchHorarios = this.horarios.filter(h => this.matcheaSiu(siuMat.nombre, h.asignatura));
|
||||||
for (const h of matchHorarios) {
|
if (matchHorarios.length === 1) {
|
||||||
if (!this.favoritos.includes(h.codigo)) {
|
const h = matchHorarios[0];
|
||||||
this.favoritos = [...this.favoritos, h.codigo];
|
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;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1115,6 +1176,58 @@ export class HomePage implements OnInit {
|
|||||||
if (changed) this.guardarProgreso();
|
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 {
|
autoAsignarOptativasSiu(): boolean {
|
||||||
if (!this.siuMaterias) return false;
|
if (!this.siuMaterias) return false;
|
||||||
let mappedAlgo = false;
|
let mappedAlgo = false;
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export class CredencialService {
|
|||||||
|
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
|
|
||||||
const scale = 2.0;
|
const scale = 3.0;
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
|||||||
@@ -185,11 +185,22 @@ export class DataService {
|
|||||||
return (hash % 2_000_000) + 100_000;
|
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[]) {
|
private async checkRoomChanges(oldData: ClaseHorario[], newData: ClaseHorario[], favoritos: string[]) {
|
||||||
for (const item of newData) {
|
for (const item of newData) {
|
||||||
if (favoritos.includes(item.codigo)) {
|
if (this.isItemInFavorites(item, favoritos)) {
|
||||||
const oldItem = oldData.find(o =>
|
const oldItem = oldData.find(o =>
|
||||||
o.codigo === item.codigo &&
|
o.codigo === item.codigo &&
|
||||||
|
o.asignatura === item.asignatura &&
|
||||||
o.dia === item.dia &&
|
o.dia === item.dia &&
|
||||||
o.horario === item.horario
|
o.horario === item.horario
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AuthService } from './auth.service';
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SiuService {
|
export class SiuService {
|
||||||
constructor(private authService: AuthService) {}
|
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[]> {
|
async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +49,27 @@ export class SiuService {
|
|||||||
|
|
||||||
const rawHtml = resCursada.data;
|
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 regex = /on_arrival\((\{.*?"id":"info_materia".*?\})\);/s;
|
||||||
const match = rawHtml.match(regex);
|
const match = rawHtml.match(regex);
|
||||||
|
|
||||||
@@ -59,7 +79,7 @@ export class SiuService {
|
|||||||
return this.parseInscripcionesActivasHtml(contentHtml);
|
return this.parseInscripcionesActivasHtml(contentHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
return []; // Quizás no tiene inscripciones activas
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error SIU Active Inscriptions:', error);
|
console.error('Error SIU Active Inscriptions:', error);
|
||||||
return [];
|
return [];
|
||||||
@@ -254,20 +274,41 @@ export class SiuService {
|
|||||||
private parseInscripcionesActivasHtml(html: string): any[] {
|
private parseInscripcionesActivasHtml(html: string): any[] {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
// Buscamos los elementos que contienen los nombres de las materias (según el HTML del usuario)
|
// Buscamos los elementos que contienen las inscripciones activas
|
||||||
const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td'));
|
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() || '';
|
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(' - ');
|
const parts = text.split(' - ');
|
||||||
if (parts.length >= 2) {
|
|
||||||
const codigo = parts[parts.length - 1].trim();
|
const codigo = parts[parts.length - 1].trim();
|
||||||
const nombre = parts.slice(0, -1).join(' - ').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[] {
|
private parseHtmlSubjects(html: string): any[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user