From f1da6175ae5e4c972c55bdfb535e0c1079e464e2 Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Fri, 27 Mar 2026 07:43:41 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20actualizaci=C3=B3n=20a=20v1.1.9=20con?= =?UTF-8?q?=20Agenda=20interactiva=20y=20correcciones=20visuales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/extensions.json | 8 +- README.md | 121 +++- android/app/build.gradle | 4 +- angular.json | 4 +- package-lock.json | 153 ++--- package.json | 2 +- .../agenda-modal/agenda-modal.component.html | 48 ++ .../agenda-modal/agenda-modal.component.scss | 77 +++ .../agenda-modal.component.spec.ts | 22 + .../agenda-modal/agenda-modal.component.ts | 68 +++ .../evento-modal/evento-modal.component.html | 80 +++ .../evento-modal/evento-modal.component.scss | 52 ++ .../evento-modal/evento-modal.component.ts | 70 +++ src/app/home/home.module.ts | 5 +- src/app/home/home.page.html | 68 ++- src/app/home/home.page.scss | 559 +++++++++++------- src/app/home/home.page.ts | 321 +++++++++- src/app/services/csv-storage.service.ts | 63 +- src/app/services/data.ts | 91 ++- src/app/services/eventos.service.ts | 113 ++++ 20 files changed, 1525 insertions(+), 404 deletions(-) create mode 100644 src/app/components/agenda-modal/agenda-modal.component.html create mode 100644 src/app/components/agenda-modal/agenda-modal.component.scss create mode 100644 src/app/components/agenda-modal/agenda-modal.component.spec.ts create mode 100644 src/app/components/agenda-modal/agenda-modal.component.ts create mode 100644 src/app/components/evento-modal/evento-modal.component.html create mode 100644 src/app/components/evento-modal/evento-modal.component.scss create mode 100644 src/app/components/evento-modal/evento-modal.component.ts create mode 100644 src/app/services/eventos.service.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1e9202d..6d2c885 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,5 @@ { - "recommendations": [ - "Webnative.webnative" - ] -} + "recommendations": [ + "Webnative.webnative" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 98aee70..6df3436 100644 --- a/README.md +++ b/README.md @@ -2,67 +2,126 @@ 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). -## Información Técnica y Frameworks +--- -* **Base Frontend**: [Angular 20.3.x](https://angular.io/) -* **Librería de Componentes UI**: [Ionic Framework v8](https://ionicframework.com/) -* **Puente Nativo Multiplataforma**: [Capacitor v8](https://capacitorjs.com/) -* **Procesamiento de Archivos PDF Interno**: `pdfjs-dist` (Renderización nativa in-dom de la Credencial) +## ✨ Características -## Plugins de Experiencia Nativa (Capacitor) +### 📋 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. -La aplicación invoca funciones 100% nativas apoyándose en las siguientes librerías core: -- `@capacitor/filesystem`: Escritura pesada persistente de respaldos en CSV en el FileSystem local ("Documents"). -- `@capawesome/capacitor-file-picker`: Interfaz de File Picker del OS (Cloud / Storage) para restauración manual. -- `@capacitor/local-notifications`: Motor de avisos visuales locales ante un cambio repentino de aula mapeada en favoritos. -- `@capacitor/preferences`: Almacenamiento rápido volátil (Key-Value) para estado de caché de credenciales offline y preferencias menores. -- `@transistorsoft/capacitor-background-fetch`: Cronjobs transparentes delegados al Sistema Operativo para despertar a la app y sincronizar la cartelera. -- `@capacitor-community/screen-brightness`: Anulación y control programático de brillo de panel al desplegar formatos estáticos de código QR/Credencial UI. -- `@capacitor/haptics`: Respuestas hápticas (Vibración inteligente) en interacciones de hardware/software clave. -- `@capacitor/share`: Puente social nativo para el envío de horarios por WhatsApp, Telegram, etc. +### ⭐ 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. + +### 🪪 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. --- -## Compatibilidad Multiplataforma (iOS y Android) +## 🛠️ Información Técnica y Frameworks -Todo el código fuente de Aulas UNICABA (TypeScript, HTML y SCSS) y la selección estricta de sus plugins de Capacitor son **100% compatibles tanto con equipos móviles Android como con el ecosistema iOS de Apple**. La arquitectura garantiza que la aplicación operará sintácticamente de forma idéntica en iPhone. +| Componente | Tecnología | +|---|---| +| 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 | + +--- + +## 📱 Compatibilidad Multiplataforma + +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. ### 🤖 Despliegue en Android + **Requisito previo**: [Android Studio](https://developer.android.com/studio) instalado, Node.js y NPM. + ```bash -# 1. Instalar dependencias e hibridación +# 1. Instalar dependencias npm install -# 2. Compilación del binario web -npm run build +# 2. Compilación del bundle web +npm run build -# 3. Transpilación y Sincronización de puentes nativos Android +# 3. Sincronización con puentes nativos npx cap sync android -# 4. Apertura del EDI para firma de APK o ejecución en celular por ADB +# 4. Abrir en Android Studio para firmar APK o ejecutar por ADB npx cap open android ``` ### 🍏 Despliegue en iOS (iPhone / iPad) -**Requisito previo**: Una computadora macOS reciente, [Xcode](https://developer.apple.com/xcode/), Node.js y CocoaPods instalado globalmente (`sudo gem install cocoapods`). + +**Requisito previo**: macOS reciente, [Xcode](https://developer.apple.com/xcode/), Node.js y CocoaPods (`sudo gem install cocoapods`). + ```bash -# 1-2. Instalación y compilación estándar de web +# 1-2. Instalación y compilación web npm install npm run build -# 3. Empaquetar el proyecto inicial para iOS (Solo se ejecuta una vez en la vida del proyecto) +# 3. Agregar plataforma iOS (solo la primera vez) npx cap add ios # 4. Sincronizar plugins y actualizar Pods npx cap sync ios -# 5. Apertura en Xcode para configurar equipo de Apple Dev y emular -npx cap open ios +# 5. Abrir en Xcode +npx cap open ios ``` -> **Nota para Xcode:** Para compilar exitosamente deberás habilitar permisos explícitos para Local Notifications y capacidades de "Background Fetch" en los *Background Modes* de la pestaña *Signing & Capabilities* dentro de Xcode. + +> **Nota para Xcode:** Habilitar permisos explícitos para **Local Notifications** y **Background Fetch** en la pestaña *Signing & Capabilities* de Xcode. --- -## Privacidad y Flujo Cero Intermediarios (Zero-Trust) -1. **Credenciales en Moodle Bypass**: Por diseño de arquitectura, la app extrae el PDF de credencial estudiantil logueándose de manera asilvestrada mediante `CapacitorHttp`. Supera bloqueos tipo CORS al delegar el flujo de HTTP al motor OS, nunca triangulando el DNI a través de un backend tercero. -2. **Resiliencia (Anti-Wipes)**: Dado las agresivas políticas *Storage Scoped* a partir de Android 11 que borran bases de datos SQLite y Preferences locales si se desinstala, la app empuja automáticamente la personalización del alumno a un archivo estandarizado `aulas_unicaba_datos.csv` a su gestor público de archivos general ("Documentos"). +## 🔒 Privacidad y Flujo Cero Intermediarios (Zero-Trust) + +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"). + diff --git a/android/app/build.gradle b/android/app/build.gradle index 0011473..28b53af 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "ar.edu.unicaba.cartelera" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 8 - versionName "1.1.5" + versionCode 12 + versionName "1.1.9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/angular.json b/angular.json index 820f541..c27b8ea 100644 --- a/angular.json +++ b/angular.json @@ -49,8 +49,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumWarning": "6kb", + "maximumError": "8kb" } ], "fileReplacements": [ diff --git a/package-lock.json b/package-lock.json index 45f8dd0..da45533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aulas-unicaba", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aulas-unicaba", - "version": "1.1.4", + "version": "1.1.5", "dependencies": { "@angular/animations": "~20.3.18", "@angular/common": "~20.3.18", @@ -600,6 +600,7 @@ "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.7.0", "eslint-scope": "^9.0.0" @@ -629,6 +630,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.18.tgz", "integrity": "sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -778,6 +780,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.18.tgz", "integrity": "sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -794,6 +797,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.18.tgz", "integrity": "sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -807,6 +811,7 @@ "integrity": "sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -839,6 +844,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.18.tgz", "integrity": "sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -864,6 +870,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.18.tgz", "integrity": "sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -892,6 +899,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.18.tgz", "integrity": "sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -932,6 +940,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.18.tgz", "integrity": "sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -976,6 +985,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2825,6 +2835,7 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.2.0.tgz", "integrity": "sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -3947,6 +3958,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -5267,9 +5279,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5290,9 +5299,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5313,9 +5319,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5336,9 +5339,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5359,9 +5359,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5559,9 +5556,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5579,9 +5573,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5599,9 +5590,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5619,9 +5607,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5639,9 +5624,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5659,9 +5641,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5679,9 +5658,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6209,9 +6185,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6233,9 +6206,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6257,9 +6227,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6281,9 +6248,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6305,9 +6269,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6329,9 +6290,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6519,9 +6477,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6536,9 +6491,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6552,9 +6504,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6568,9 +6517,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6585,9 +6531,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6602,9 +6545,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6619,9 +6559,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6636,9 +6573,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6653,9 +6587,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6670,9 +6601,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6687,9 +6615,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6703,9 +6628,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6719,9 +6641,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7162,6 +7081,7 @@ "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -7302,6 +7222,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -7409,6 +7330,7 @@ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7481,6 +7403,7 @@ "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", @@ -7765,6 +7688,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7840,6 +7764,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8538,6 +8463,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10197,6 +10123,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10689,6 +10616,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11477,6 +11405,7 @@ "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -12559,7 +12488,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jasmine-spec-reporter": { "version": "5.0.2", @@ -12608,6 +12538,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12748,6 +12679,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -13325,6 +13257,7 @@ "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -13469,6 +13402,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -15366,6 +15300,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16063,9 +15998,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -16080,9 +16012,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -16097,9 +16026,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -16114,9 +16040,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -16210,6 +16133,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -16303,6 +16227,7 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17445,6 +17370,7 @@ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -17646,7 +17572,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -17782,6 +17709,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18047,6 +17975,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -18181,6 +18110,7 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18283,6 +18213,7 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -19265,6 +19196,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19283,7 +19215,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/package.json b/package.json index a34dd2a..c6922ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aulas-unicaba", - "version": "1.1.5", + "version": "1.1.9", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { diff --git a/src/app/components/agenda-modal/agenda-modal.component.html b/src/app/components/agenda-modal/agenda-modal.component.html new file mode 100644 index 0000000..b12164f --- /dev/null +++ b/src/app/components/agenda-modal/agenda-modal.component.html @@ -0,0 +1,48 @@ + + + Agenda Académica + + Cerrar + + + + + +
+

{{ materia.asignatura }}

+

Código: {{ materia.codigo }}

+
+ + + + Agendar Nuevo Evento + + +
+ +

No hay eventos programados.

+
+ + + +
+
+ {{ ev.tipo }} +
+
+

+ {{ formatFecha(ev.fecha) }} + {{ formatHora(ev.fecha) }} hs +

+

+ Aula: {{ ev.aula }} +

+

+ {{ ev.notas }} +

+
+
+ +
+
+
diff --git a/src/app/components/agenda-modal/agenda-modal.component.scss b/src/app/components/agenda-modal/agenda-modal.component.scss new file mode 100644 index 0000000..817ad47 --- /dev/null +++ b/src/app/components/agenda-modal/agenda-modal.component.scss @@ -0,0 +1,77 @@ +.materia-info { + margin-bottom: 20px; + h2 { + font-size: 1.2rem; + font-weight: 700; + color: var(--ion-text-color); + margin-bottom: 4px; + } + p { + font-size: 0.9rem; + color: var(--ion-color-medium); + margin: 0; + } +} + +.add-btn { + margin-bottom: 20px; + --border-radius: 8px; +} + +.empty-state { + text-align: center; + margin-top: 40px; + color: var(--ion-color-medium); + + ion-icon { + font-size: 3rem; + margin-bottom: 10px; + opacity: 0.5; + } +} + +.agenda-list { + background: transparent; +} + +.agenda-item { + --background: var(--ion-card-background, #ffffff); + --border-radius: 8px; + margin-bottom: 10px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + + .item-content { + width: 100%; + padding: 10px 0; + } + + .item-header { + margin-bottom: 8px; + } + + .item-details { + p { + margin: 4px 0; + font-size: 0.9rem; + color: var(--ion-text-color); + display: flex; + align-items: center; + + ion-icon { + margin-right: 6px; + color: var(--ion-color-primary); + } + + &.notas { + color: var(--ion-color-medium); + font-style: italic; + margin-top: 8px; + font-size: 0.85rem; + } + } + + .icon-space { + margin-left: 12px; + } + } +} diff --git a/src/app/components/agenda-modal/agenda-modal.component.spec.ts b/src/app/components/agenda-modal/agenda-modal.component.spec.ts new file mode 100644 index 0000000..3e0eabc --- /dev/null +++ b/src/app/components/agenda-modal/agenda-modal.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { AgendaModalComponent } from './agenda-modal.component'; + +describe('AgendaModalComponent', () => { + let component: AgendaModalComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [AgendaModalComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AgendaModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/agenda-modal/agenda-modal.component.ts b/src/app/components/agenda-modal/agenda-modal.component.ts new file mode 100644 index 0000000..f86e9c7 --- /dev/null +++ b/src/app/components/agenda-modal/agenda-modal.component.ts @@ -0,0 +1,68 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { EventoAcademico, ClaseHorario } from '../../services/data'; +import { EventosService } from '../../services/eventos.service'; +import { EventoModalComponent } from '../evento-modal/evento-modal.component'; + +@Component({ + selector: 'app-agenda-modal', + templateUrl: './agenda-modal.component.html', + styleUrls: ['./agenda-modal.component.scss'], + standalone: true, + imports: [CommonModule, IonicModule] +}) +export class AgendaModalComponent implements OnInit { + @Input() materia!: ClaseHorario; + eventos: EventoAcademico[] = []; + + constructor( + private modalCtrl: ModalController, + private eventosService: EventosService + ) {} + + ngOnInit() { + this.cargarEventos(); + } + + ionViewWillEnter() { + this.cargarEventos(); + } + + cargarEventos() { + this.eventos = this.eventosService.getEventosByMateria(this.materia.codigo) + .sort((a, b) => new Date(a.fecha).getTime() - new Date(b.fecha).getTime()); + } + + dismiss() { + this.modalCtrl.dismiss(); + } + + formatFecha(fechaIso: string): string { + const d = new Date(fechaIso); + return d.toLocaleDateString([], { day: '2-digit', month: '2-digit', year: 'numeric' }); + } + + formatHora(fechaIso: string): string { + const d = new Date(fechaIso); + return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + + async abrirEditor(evento?: EventoAcademico) { + const modal = await this.modalCtrl.create({ + component: EventoModalComponent, + componentProps: { + materiaCodigo: this.materia.codigo, + materiaNombre: this.materia.asignatura, + eventoExistente: evento + } + }); + + await modal.present(); + + const { role } = await modal.onWillDismiss(); + if (role === 'saved' || role === 'deleted') { + this.cargarEventos(); + } + } +} diff --git a/src/app/components/evento-modal/evento-modal.component.html b/src/app/components/evento-modal/evento-modal.component.html new file mode 100644 index 0000000..9560e00 --- /dev/null +++ b/src/app/components/evento-modal/evento-modal.component.html @@ -0,0 +1,80 @@ + + + {{ isEdit ? 'Editar' : 'Programar' }} Hito + + Cerrar + + + + + +
+ Materia: +

{{ materiaNombre }}

+
+ + + + Tipo e Hito + + Parcial 1 + Parcial 2 + Trabajo Práctico 1 + Trabajo Práctico 2 + Trabajo Práctico 3 + Recuperatorio Parcial 1 + Recuperatorio Parcial 2 + Recuperatorio TP 1 + Recuperatorio TP 2 + Recuperatorio TP 3 + Final + Otro + + + + + Fecha y Hora + + + + + + + + + + + + + Aula / Zoom + + + + + + Link + + + + + + + + Notas + + + + + +
+ + {{ isEdit ? 'Actualizar Agenda' : 'Agendar Hito' }} + + + + + Eliminar Evento + +
+
\ No newline at end of file diff --git a/src/app/components/evento-modal/evento-modal.component.scss b/src/app/components/evento-modal/evento-modal.component.scss new file mode 100644 index 0000000..8bb138d --- /dev/null +++ b/src/app/components/evento-modal/evento-modal.component.scss @@ -0,0 +1,52 @@ +.materia-info { + margin-top: 10px; + h3 { + margin: 5px 0 20px; + font-size: 1.4rem; + font-weight: 700; + } +} + +.input-item { + --padding-start: 10px; + --padding-end: 10px; + --padding-top: 5px; + --padding-bottom: 5px; + --min-height: 45px; + margin-bottom: 10px; + border-radius: 8px; + background: var(--ion-color-light); + + ion-label { + font-weight: 600; + margin-bottom: 4px !important; + font-size: 0.85rem; + } +} + +.side-item { + margin-left: 2px; + margin-right: 2px; +} + +.compact-list { + padding-top: 0; +} + + +ion-datetime-button { + margin-top: 8px; + margin-bottom: 8px; +} + +.actions-container { + margin-top: 30px; +} + +.main-btn { + margin-bottom: 15px; +} + +.delete-btn { + --color: var(--ion-color-danger); +} diff --git a/src/app/components/evento-modal/evento-modal.component.ts b/src/app/components/evento-modal/evento-modal.component.ts new file mode 100644 index 0000000..d072491 --- /dev/null +++ b/src/app/components/evento-modal/evento-modal.component.ts @@ -0,0 +1,70 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { EventoAcademico } from '../../services/data'; +import { EventosService } from '../../services/eventos.service'; + + +@Component({ + selector: 'app-evento-modal', + templateUrl: './evento-modal.component.html', + styleUrls: ['./evento-modal.component.scss'], + standalone: true, + imports: [CommonModule, IonicModule, FormsModule] +}) +export class EventoModalComponent implements OnInit { + @Input() materiaCodigo!: string; + @Input() materiaNombre!: string; + @Input() eventoExistente?: EventoAcademico; + + tipo: 'Parcial' | 'TP' | 'Final' | 'Otro' = 'Parcial'; + fecha: string = new Date().toISOString(); + aula: string = ''; + link: string = ''; + notas: string = ''; + + isEdit: boolean = false; + + constructor( + private modalCtrl: ModalController, + private eventosService: EventosService + ) { } + + ngOnInit() { + if (this.eventoExistente) { + this.isEdit = true; + this.tipo = this.eventoExistente.tipo; + this.fecha = this.eventoExistente.fecha; + this.aula = this.eventoExistente.aula || ''; + this.link = this.eventoExistente.link || ''; + this.notas = this.eventoExistente.notas || ''; + } + } + + dismiss() { + this.modalCtrl.dismiss(); + } + + async guardar() { + const nuevoEvento: EventoAcademico = { + id: this.isEdit ? this.eventoExistente!.id : Date.now().toString(), + materiaCodigo: this.materiaCodigo, + tipo: this.tipo, + fecha: this.fecha, + aula: this.aula, + link: this.link, + notas: this.notas + }; + + await this.eventosService.saveEvento(nuevoEvento); + this.modalCtrl.dismiss(nuevoEvento, 'saved'); + } + + async eliminar() { + if (this.eventoExistente) { + await this.eventosService.deleteEvento(this.eventoExistente.id); + this.modalCtrl.dismiss(null, 'deleted'); + } + } +} diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index ac59d9f..8298083 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -6,6 +6,8 @@ import { HomePage } from './home.page'; import { HomePageRoutingModule } from './home-routing.module'; import { CredencialModalComponent } from '../components/credencial-modal/credencial-modal.component'; +import { EventoModalComponent } from '../components/evento-modal/evento-modal.component'; + @NgModule({ @@ -14,7 +16,8 @@ import { CredencialModalComponent } from '../components/credencial-modal/credenc FormsModule, IonicModule, HomePageRoutingModule, - CredencialModalComponent + CredencialModalComponent, + EventoModalComponent ], declarations: [HomePage] }) diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index 41fd7c6..10e3a87 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -13,7 +13,7 @@ - +
@@ -71,7 +71,7 @@ realiza en la memoria de tu teléfono.

Sin Rastreadores: No incluimos publicidad, telemetría ni analíticas de terceros.

-

Versión 1.1.5 - 25/03/2026

+

Versión 1.1.9 - 26/03/2026

@@ -82,6 +82,38 @@ refreshingText="Actualizando..."> + +
+ + +
+
+ + {{ proximaClase.label }} +
+ + + {{ formatCountdown(proximaClase.diffMs) }} + +
+ +

{{ proximaClase.item.asignatura }}

+
+ + + Aula {{ proximaClase.item.aula }} + + + + {{ proximaClase.item.dia }} {{ proximaClase.item.horario }} + +
+ +
+
+
+

Cargando cartelera...

@@ -102,26 +134,43 @@ {{ item.asignatura }}
+ - + + +
+ + + +
+ + + {{ proximo.tipo }}: {{ formatFechaEvento(proximo.fecha) }} + +
+ - +

Código: {{ item.codigo }}

{{ item.aula }}

{{ item.dia }} - {{ item.horario }}

+ @@ -133,9 +182,12 @@ -