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
|
||||
- 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").
|
||||
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aulas-unicaba",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"author": "Ionic Framework",
|
||||
"homepage": "https://ionicframework.com/",
|
||||
"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.
|
||||
|
||||
---
|
||||
*Ú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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user