feat(v1.2.1): Integración nativa de Google Calendar UI y exportación dual offline WebView/Share

This commit is contained in:
2026-03-27 16:37:42 -03:00
parent f1da6175ae
commit ca33ed19ce
67 changed files with 995 additions and 285 deletions
+9
View File
@@ -34,6 +34,15 @@ Un visor híbrido especializado y offline-first desarrollado para la visualizaci
- Usa IDs de notificación estables para evitar duplicados al sincronizar múltiples veces. - Usa IDs de notificación estables para evitar duplicados al sincronizar múltiples veces.
- Funciona completamente **offline** — no requiere servidor externo. - 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 ### 🪪 Credencial Digital Universitaria
- Inicio de sesión directo al Campus Virtual (Moodle) con DNI y clave. - 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. - **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.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "ar.edu.unicaba.cartelera" applicationId "ar.edu.unicaba.cartelera"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 12 versionCode 14
versionName "1.1.9" versionName "1.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation project(':capacitor-share') implementation project(':capacitor-share')
implementation project(':capacitor-status-bar') implementation project(':capacitor-status-bar')
implementation project(':capawesome-capacitor-file-picker') implementation project(':capawesome-capacitor-file-picker')
implementation project(':ebarooni-capacitor-calendar')
implementation project(':transistorsoft-capacitor-background-fetch') implementation project(':transistorsoft-capacitor-background-fetch')
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+3
View File
@@ -38,5 +38,8 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
include ':capawesome-capacitor-file-picker' include ':capawesome-capacitor-file-picker'
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android') project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
include ':ebarooni-capacitor-calendar'
project(':ebarooni-capacitor-calendar').projectDir = new File('../node_modules/@ebarooni/capacitor-calendar/android')
include ':transistorsoft-capacitor-background-fetch' include ':transistorsoft-capacitor-background-fetch'
project(':transistorsoft-capacitor-background-fetch').projectDir = new File('../node_modules/@transistorsoft/capacitor-background-fetch/android') project(':transistorsoft-capacitor-background-fetch').projectDir = new File('../node_modules/@transistorsoft/capacitor-background-fetch/android')
+2 -2
View File
@@ -49,8 +49,8 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "6kb", "maximumWarning": "10kb",
"maximumError": "8kb" "maximumError": "12kb"
} }
], ],
"fileReplacements": [ "fileReplacements": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.
+15 -43
View File
@@ -1,12 +1,12 @@
{ {
"name": "aulas-unicaba", "name": "aulas-unicaba",
"version": "1.1.5", "version": "1.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aulas-unicaba", "name": "aulas-unicaba",
"version": "1.1.5", "version": "1.2.1",
"dependencies": { "dependencies": {
"@angular/animations": "~20.3.18", "@angular/animations": "~20.3.18",
"@angular/common": "~20.3.18", "@angular/common": "~20.3.18",
@@ -30,6 +30,7 @@
"@capacitor/share": "^8.0.1", "@capacitor/share": "^8.0.1",
"@capacitor/status-bar": "8.0.1", "@capacitor/status-bar": "8.0.1",
"@capawesome/capacitor-file-picker": "^8.0.2", "@capawesome/capacitor-file-picker": "^8.0.2",
"@ebarooni/capacitor-calendar": "^8.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@transistorsoft/capacitor-background-fetch": "^8.0.0", "@transistorsoft/capacitor-background-fetch": "^8.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
@@ -600,7 +601,6 @@
"integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@angular-eslint/bundled-angular-compiler": "20.7.0", "@angular-eslint/bundled-angular-compiler": "20.7.0",
"eslint-scope": "^9.0.0" "eslint-scope": "^9.0.0"
@@ -630,7 +630,6 @@
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.18.tgz",
"integrity": "sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==", "integrity": "sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -780,7 +779,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.18.tgz",
"integrity": "sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw==", "integrity": "sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -797,7 +795,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.18.tgz",
"integrity": "sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==", "integrity": "sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -811,7 +808,6 @@
"integrity": "sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g==", "integrity": "sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -844,7 +840,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.18.tgz",
"integrity": "sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw==", "integrity": "sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -870,7 +865,6 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.18.tgz",
"integrity": "sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg==", "integrity": "sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -899,7 +893,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.18.tgz",
"integrity": "sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ==", "integrity": "sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -940,7 +933,6 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.18.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.18.tgz",
"integrity": "sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==", "integrity": "sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -985,7 +977,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -2835,7 +2826,6 @@
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.2.0.tgz", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.2.0.tgz",
"integrity": "sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==", "integrity": "sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -2960,6 +2950,15 @@
"node": ">=14.17.0" "node": ">=14.17.0"
} }
}, },
"node_modules/@ebarooni/capacitor-calendar": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@ebarooni/capacitor-calendar/-/capacitor-calendar-8.0.1.tgz",
"integrity": "sha512-kwtvdYrcogioAkoft5Pcj/dkgsCaBUc4WVf3WoRBYQ92frfr4aNUSe+2Sd8ioA9ghrYAAdIPzTWzU4+tLU62iQ==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@es-joy/jsdoccomment": { "node_modules/@es-joy/jsdoccomment": {
"version": "0.46.0", "version": "0.46.0",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",
@@ -3958,7 +3957,6 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@inquirer/checkbox": "^4.2.1", "@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14", "@inquirer/confirm": "^5.1.14",
@@ -7081,7 +7079,6 @@
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
@@ -7222,7 +7219,6 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1", "@typescript-eslint/types": "8.56.1",
@@ -7330,7 +7326,6 @@
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
@@ -7403,7 +7398,6 @@
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/scope-manager": "8.56.1",
@@ -7688,7 +7682,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -7764,7 +7757,6 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -8463,7 +8455,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -10123,7 +10114,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -10616,7 +10606,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -11405,7 +11394,6 @@
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -12488,8 +12476,7 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/jasmine-spec-reporter": { "node_modules/jasmine-spec-reporter": {
"version": "5.0.2", "version": "5.0.2",
@@ -12538,7 +12525,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -12679,7 +12665,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0", "@colors/colors": "1.5.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -13257,7 +13242,6 @@
"integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"copy-anything": "^2.0.1", "copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1", "parse-node-version": "^1.0.1",
@@ -13402,7 +13386,6 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
@@ -15300,7 +15283,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -16133,7 +16115,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -16227,7 +16208,6 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -17370,7 +17350,6 @@
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0", "acorn": "^8.14.0",
@@ -17572,8 +17551,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"peer": true
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "4.1.0", "version": "4.1.0",
@@ -17709,7 +17687,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -17975,7 +17952,6 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -18110,7 +18086,6 @@
"integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8", "@types/estree": "^1.0.8",
@@ -18213,7 +18188,6 @@
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/bonjour": "^3.5.13", "@types/bonjour": "^3.5.13",
"@types/connect-history-api-fallback": "^1.5.4", "@types/connect-history-api-fallback": "^1.5.4",
@@ -19196,7 +19170,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -19215,8 +19188,7 @@
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT", "license": "MIT"
"peer": true
} }
} }
} }
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aulas-unicaba", "name": "aulas-unicaba",
"version": "1.1.9", "version": "1.2.1",
"author": "Ionic Framework", "author": "Ionic Framework",
"homepage": "https://ionicframework.com/", "homepage": "https://ionicframework.com/",
"scripts": { "scripts": {
@@ -35,6 +35,7 @@
"@capacitor/share": "^8.0.1", "@capacitor/share": "^8.0.1",
"@capacitor/status-bar": "8.0.1", "@capacitor/status-bar": "8.0.1",
"@capawesome/capacitor-file-picker": "^8.0.2", "@capawesome/capacitor-file-picker": "^8.0.2",
"@ebarooni/capacitor-calendar": "^8.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@transistorsoft/capacitor-background-fetch": "^8.0.0", "@transistorsoft/capacitor-background-fetch": "^8.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
+4 -3
View File
@@ -6,7 +6,8 @@ Esta sección detalla el funcionamiento, alcance y compromiso de privacidad de l
**Aulas UNICABA** es un visor especializado y autónomo para la cartelera de horarios, aulas y credencial de la Universidad de la Ciudad. **Aulas UNICABA** es un visor especializado y autónomo para la cartelera de horarios, aulas y credencial de la Universidad de la Ciudad.
- Permite consultar las materias, sus respectivos días, horarios y aulas asignadas. - Permite consultar las materias, sus respectivos días, horarios y aulas asignadas.
- Facilita el seguimiento de materias específicas mediante un sistema de **Favoritos**. - Facilita el seguimiento de materias específicas mediante un sistema de **Favoritos**.
- Avisa mediante **Notificaciones locales** si se detecta un cambio de aula en las materias marcadas como favoritas. - Avisa mediante **Notificaciones locales** si se detecta un cambio de aula en las materias marcadas como favoritas, y emite **Recordatorios Automáticos** programados 30 minutos antes de cada clase.
- Gestiona una **Agenda de Exámenes e Hitos** integrada por materia (Parciales, Finales, TP), cuyos datos nunca abandonan tu celular.
- **Opcionalmente**, permite acceder a tu **Credencial Digital** universitaria de los registros oficiales de forma segura e integrada. - **Opcionalmente**, permite acceder a tu **Credencial Digital** universitaria de los registros oficiales de forma segura e integrada.
## 2. ¿Cómo funciona? (Alcance Técnico) ## 2. ¿Cómo funciona? (Alcance Técnico)
@@ -19,11 +20,11 @@ La aplicación ha sido diseñada bajo una arquitectura de "cliente liviano" con
## 3. Privacidad Absoluta y Persistencia Avanzada ## 3. Privacidad Absoluta y Persistencia Avanzada
- **Sin Intermediarios ni Datos Personales:** No retenemos u observamos nombres, correos ni datos sensibles vinculados a las cuentas y accesos del usuario. Ni Aulas UNICABA ni empresas externas rastrearán o vincularán tu cuenta. - **Sin Intermediarios ni Datos Personales:** No retenemos u observamos nombres, correos ni datos sensibles vinculados a las cuentas y accesos del usuario. Ni Aulas UNICABA ni empresas externas rastrearán o vincularán tu cuenta.
- **Sin Rastreadores (Zero Trackers):** Nuestra filosofía es estricta. Cero herramientas analíticas (No Google Analytics, no SDKs invasivos de Facebook o reportes crudos), sin métricas engorrosas y libre de publicidad en el código fuente. - **Sin Rastreadores (Zero Trackers):** Nuestra filosofía es estricta. Cero herramientas analíticas (No Google Analytics, no SDKs invasivos de Facebook o reportes crudos), sin métricas engorrosas y libre de publicidad en el código fuente.
- **Persistencia Local Segura (CSV Offline):** Valoramos tus elecciones. Tus decisiones (Materias Favoritas) se guardan directamente como respaldo silencioso bajo un formato `.csv` estandarizado en la carpeta nativa interna "Documentos" del dispositivo (Respaldado por permisos explícitos de Almacenamiento y `Filesystem API`). **Esto permite que tus favoritos (y futuras agendas de exámenes) asombrosamente sobrevivan a desinstalaciones agresivas de la app o borrado de datos** sin depender y jamás tocar entornos remotos en la Nube. - **Persistencia Local Segura (CSV Offline):** Valoramos tus elecciones. Tus decisiones (Materias Favoritas y Agenda de Exámenes) se guardan directamente como respaldo silencioso bajo un formato `.csv` estandarizado en la carpeta nativa interna "Documentos" del dispositivo (Respaldado por permisos explícitos de Almacenamiento y `Filesystem API`). **Esto permite que tus favoritos y tu agenda asombrosamente sobrevivan a desinstalaciones agresivas de la app o borrado de datos** sin depender y jamás tocar entornos remotos en la Nube.
- **Caché Inteligente (Modo Offline):** Entendemos que la conectividad en facultades y subtes es delicada. Toda la cartelera central y tu Credencial QR se empaquetan en `Base64` utilizando memorias volátiles locales (`Preferences API`). Es decir, puedes mostrar qué materia debes rendir o exhibir tu credencial QR instantáneamente sin red (Modo Avión o Sin Señal). - **Caché Inteligente (Modo Offline):** Entendemos que la conectividad en facultades y subtes es delicada. Toda la cartelera central y tu Credencial QR se empaquetan en `Base64` utilizando memorias volátiles locales (`Preferences API`). Es decir, puedes mostrar qué materia debes rendir o exhibir tu credencial QR instantáneamente sin red (Modo Avión o Sin Señal).
## 4. Compromiso de Transparencia ## 4. Compromiso de Transparencia
Esta innovación tecnológica es de carácter informativo. La precisión estricta de asignación de las aulas depende originariamente de la pre-carga impuesta por el cuerpo no-docente en el documento de Spreadsheets. En escenarios contradictorios, la palabra oficial del ala administrativa universitaria primará sobre la respuesta sincronizada en Aulas UNICABA. Esta innovación tecnológica es de carácter informativo. La precisión estricta de asignación de las aulas depende originariamente de la pre-carga impuesta por el cuerpo no-docente en el documento de Spreadsheets. En escenarios contradictorios, la palabra oficial del ala administrativa universitaria primará sobre la respuesta sincronizada en Aulas UNICABA.
--- ---
*Última actualización: 25 de Marzo, 2026 - v1.1.5* *Última actualización: 27 de Marzo, 2026 - v1.1.9*
@@ -32,16 +32,32 @@
</ion-select> </ion-select>
</ion-item> </ion-item>
<ion-item class="input-item"> <ion-grid class="ion-no-padding">
<ion-label position="stacked">Fecha y Hora</ion-label> <ion-row>
<ion-datetime-button datetime="datetime"></ion-datetime-button> <ion-col size="6">
<ion-modal [keepContentsMounted]="true"> <ion-item class="input-item side-item">
<ion-label position="stacked">Día del Hito</ion-label>
<ion-datetime-button datetime="date-input"></ion-datetime-button>
<ion-popover [keepContentsMounted]="true">
<ng-template> <ng-template>
<ion-datetime id="datetime" [(ngModel)]="fecha" presentation="date-time" [showDefaultButtons]="true" <ion-datetime id="date-input" [(ngModel)]="fecha" presentation="date" prefer-wheel="true" [showDefaultButtons]="true" doneText="OK" cancelText="Atrás"></ion-datetime>
doneText="OK" cancelText="Cancelar"></ion-datetime>
</ng-template> </ng-template>
</ion-modal> </ion-popover>
</ion-item> </ion-item>
</ion-col>
<ion-col size="6">
<ion-item class="input-item side-item">
<ion-label position="stacked">Hora aprox.</ion-label>
<ion-datetime-button datetime="time-input"></ion-datetime-button>
<ion-popover [keepContentsMounted]="true">
<ng-template>
<ion-datetime id="time-input" [(ngModel)]="fecha" presentation="time" prefer-wheel="true" [showDefaultButtons]="true" doneText="OK" cancelText="Atrás"></ion-datetime>
</ng-template>
</ion-popover>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
<ion-grid class="ion-no-padding"> <ion-grid class="ion-no-padding">
<ion-row> <ion-row>
@@ -72,6 +88,11 @@
{{ isEdit ? 'Actualizar Agenda' : 'Agendar Hito' }} {{ isEdit ? 'Actualizar Agenda' : 'Agendar Hito' }}
</ion-button> </ion-button>
<ion-button expand="block" fill="outline" color="dark" (click)="exportarCalendario()" class="export-btn" style="margin-top: 10px;">
<ion-icon name="calendar-outline" slot="start"></ion-icon>
Añadir a Google Calendar
</ion-button>
<ion-button *ngIf="isEdit" expand="block" fill="clear" color="danger" (click)="eliminar()" class="delete-btn"> <ion-button *ngIf="isEdit" expand="block" fill="clear" color="danger" (click)="eliminar()" class="delete-btn">
<ion-icon name="trash-outline" slot="start"></ion-icon> <ion-icon name="trash-outline" slot="start"></ion-icon>
Eliminar Evento Eliminar Evento
@@ -1,9 +1,10 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { IonicModule, ModalController } from '@ionic/angular'; import { IonicModule, ModalController, AlertController, ToastController } from '@ionic/angular';
import { EventoAcademico } from '../../services/data'; import { EventoAcademico } from '../../services/data';
import { EventosService } from '../../services/eventos.service'; import { EventosService } from '../../services/eventos.service';
import { CapacitorCalendar } from '@ebarooni/capacitor-calendar';
@Component({ @Component({
@@ -28,7 +29,9 @@ export class EventoModalComponent implements OnInit {
constructor( constructor(
private modalCtrl: ModalController, private modalCtrl: ModalController,
private eventosService: EventosService private eventosService: EventosService,
private alertCtrl: AlertController,
private toastCtrl: ToastController
) { } ) { }
ngOnInit() { ngOnInit() {
@@ -63,8 +66,52 @@ export class EventoModalComponent implements OnInit {
async eliminar() { async eliminar() {
if (this.eventoExistente) { if (this.eventoExistente) {
await this.eventosService.deleteEvento(this.eventoExistente.id); const alert = await this.alertCtrl.create({
header: 'Eliminar Evento',
message: '¿Estás seguro que querés eliminar este hito de tu agenda?',
buttons: [
{
text: 'Cancelar',
role: 'cancel'
},
{
text: 'Eliminar',
role: 'destructive',
handler: async () => {
await this.eventosService.deleteEvento(this.eventoExistente!.id);
this.modalCtrl.dismiss(null, 'deleted'); this.modalCtrl.dismiss(null, 'deleted');
} }
} }
]
});
await alert.present();
}
}
async exportarCalendario() {
try {
const startDate = new Date(this.fecha);
const endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000); // +2 horas por defecto
let descString = this.notas || '';
if (this.link) descString += `\n\nLink de cursada: ${this.link}`;
await CapacitorCalendar.createEventWithPrompt({
title: `${this.tipo} de ${this.materiaNombre}`,
description: descString,
location: this.aula ? `Aula ${this.aula}, Universidad de la Ciudad` : 'Universidad de la Ciudad',
startDate: startDate.getTime(),
endDate: endDate.getTime(),
isAllDay: false
});
} catch (e) {
console.error('Error abriendo enlace de Calendario:', e);
const toast = await this.toastCtrl.create({
message: 'No pudimos abrir tu Calendario. Verificá si tenés una app instalada.',
duration: 2500, color: 'warning'
});
toast.present();
}
}
} }
+116 -33
View File
@@ -7,6 +7,9 @@
Cartelera de Aulas Cartelera de Aulas
</ion-title> </ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button *ngIf="viewMode === 'favoritos'" (click)="importarRespaldoManual()" title="Importar Respaldo">
<ion-icon slot="icon-only" name="cloud-download-outline"></ion-icon>
</ion-button>
<ion-button (click)="isInfoModalOpen = true"> <ion-button (click)="isInfoModalOpen = true">
<ion-icon slot="icon-only" name="information-circle-outline"></ion-icon> <ion-icon slot="icon-only" name="information-circle-outline"></ion-icon>
</ion-button> </ion-button>
@@ -19,6 +22,7 @@
<ion-segment [(ngModel)]="viewMode" (ionChange)="onViewModeChange()" color="primary"> <ion-segment [(ngModel)]="viewMode" (ionChange)="onViewModeChange()" color="primary">
<ion-segment-button value="favoritos">Favoritos</ion-segment-button> <ion-segment-button value="favoritos">Favoritos</ion-segment-button>
<ion-segment-button value="todas">Cartelera</ion-segment-button> <ion-segment-button value="todas">Cartelera</ion-segment-button>
<ion-segment-button value="plan">Mi Carrera</ion-segment-button>
</ion-segment> </ion-segment>
</div> </div>
</ion-toolbar> </ion-toolbar>
@@ -43,40 +47,58 @@
</ion-header> </ion-header>
<ion-content class="ion-padding privacy-content"> <ion-content class="ion-padding privacy-content">
<h2>¿Qué hace esta aplicación?</h2> <h2>¿Qué hace esta aplicación?</h2>
<p><strong>Aulas UNICABA</strong> es un visor especializado e independiente para la cartelera de horarios, aulas y <p><strong>Aulas UNICABA</strong> es el visor especializado, independiente y <strong>offline-first</strong> para
uso de credencial de la Universidad de la Ciudad.</p> la cartelera de horarios, campus virtual y seguimiento de tu progreso académico de la Universidad de la Ciudad.
Desarrollado pensando en
el uso en movimiento, el ahorro de batería y la mala conectividad.</p>
<h3>Nuevas Características y Funciones</h3>
<ul> <ul>
<li>Consulta materias, horarios y aulas rápidamente.</li> <li><strong>Cartelera Rápida y Offline:</strong> Consulta de materias organizada por días y cacheada al
<li>Sigue tus <strong>Favoritos</strong> y recibe <strong>alertas locales</strong> si cambian de aula.</li> instante. Muestra datos rápido aunque estés bajo tierra sin red.</li>
<li><strong>¡NUEVO!</strong> Visualiza tu <strong>Credencial Digital</strong> de estudiante tocando el logo de <li><strong>Mi Carrera y Trackeo:</strong> Ingresá a tu Plan de Estudios interactivo. Usá los Action Sheets
la UNICABA en la cabecera.</li> nativos para marcar materias como <strong>'Aprobadas'</strong> (Fucsia) o <strong>'En Curso'</strong>
(Violeta), y gestioná tus <strong>Materias Optativas</strong> desde el catálogo
directamente con un clic.</li>
<li><strong>Próxima Clase Inteligente:</strong> Un banner destacado superior en Favoritos te avisa cuándo es y
cuánto falta
para tu próxima clase, con un reloj regresivo dinámico en vivo.</li>
<li><strong>Alertas Automáticas:</strong> Si la universidad cambia repentinamente el número de aula o el link de
Meet/Zoom de tus materias, serás notificado. También recibís pre-alertas de 30 minutos antes de cursar.</li>
</ul> </ul>
<h2>Credencial Digital (Opcional)</h2> <h3>Agenda de Hitos Académicos</h3>
<p>El inicio de sesión para obtener la credencial es <strong>100% opcional</strong> y completamente independiente <ul>
de la cartelera de aulas (que puedes seguir usando sin loguearte).</p> <li>Accedé rápidamente para agendar <strong>Parciales / Trabajos Prácticos / Finales</strong> desde cualquier
<p>Tus credenciales (DNI y Clave) se comunican <strong>directamente</strong> con el Campus Virtual (Moodle) para materia para llevar un control estricto.</li>
descargar el PDF de tu código QR, sin pasar por ningún servidor intermediario. Luego, la imagen queda guardada <li>El robusto sistema de calendario nativo "Wheels" evitará borrados accidentales de tus fechas importantes.
localmente en tu celular para que puedas mostrarla incluso sin conexión a internet.</p> </li>
</ul>
<h2>Almacenamiento Seguro y Persistente</h2> <h3>Credencial Inmediata y Exportación</h3>
<p>Tus datos son tuyos. Aseguramos que tus materias favoritas se guarden automáticamente en un archivo <ul>
<code>.csv</code> seguro dentro de la carpeta "Documentos" de tu dispositivo. <li><strong>Credencial Digital:</strong> Tocando el logo superior izquierdo obtenés tu QR offline y se sube el
brillo de tu pantalla al máximo de forma automática para facilitar la lectura.</li>
<li><strong>Respaldo Portable Seguro:</strong> Apretando la "Nubecita" o "Disquete" podés emitir un archivo de
backup (JSON seguro) hacia la memoria o nube de tu teléfono, para que restaures de inmediato toda la
configuración si mudás a otro equipo.</li>
</ul>
<h3>Privacidad Zero-Trust</h3>
<p><strong>Sin Servidores Intermediarios:</strong> No dependemos de bases de datos centralizadas, ni recopilamos
credenciales. Cada sincronización viaja directo desde Aulas UNICABA hasta el servidor del SIU/Google localmente.
</p> </p>
<p>Gracias a esto, toda tu configuración y datos están a salvo y <strong>sobrevivirán a desinstalaciones</strong>
o actualizaciones de la aplicación sin tener que subirlos a la nube.</p>
<h2>Privacidad Absoluta</h2> <p class="ion-text-center" style="margin-top: 30px;"><small>Tu ecosistema digital universitario, diseñado por y
<p><strong>Sin Intermediarios:</strong> No recopilamos nombres, contraseñas, ni correos. Todo el procesamiento se para estudiantes.</small></p>
realiza en la memoria de tu teléfono.</p>
<p><strong>Sin Rastreadores:</strong> No incluimos publicidad, telemetría ni analíticas de terceros.</p> <p class="ion-text-center" style="margin-top: 30px;"><small><strong>Versión 1.2.1</strong></small></p>
<p class="ion-text-center"><small>Versión 1.1.9 - 26/03/2026</small></p>
</ion-content> </ion-content>
</ng-template> </ng-template>
</ion-modal> </ion-modal>
<ion-content [fullscreen]="true" color="light"> <ion-content [fullscreen]="true" color="light" (swipe)="onSwipe($event)">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="Desliza para actualizar" refreshingSpinner="crescent" <ion-refresher-content pullingText="Desliza para actualizar" refreshingSpinner="crescent"
refreshingText="Actualizando..."></ion-refresher-content> refreshingText="Actualizando..."></ion-refresher-content>
@@ -123,7 +145,7 @@
<p>{{ errorMsg }}</p> <p>{{ errorMsg }}</p>
</div> </div>
<ion-list *ngIf="!loading && !errorMsg && horariosFiltrados.length > 0"> <ion-list *ngIf="!loading && !errorMsg && horariosFiltrados.length > 0 && viewMode !== 'plan'">
<ion-card *ngFor="let item of horariosFiltrados" [class.highlight-card]="item.codigo === highlightedCodigo" <ion-card *ngFor="let item of horariosFiltrados" [class.highlight-card]="item.codigo === highlightedCodigo"
class="subject-card"> class="subject-card">
@@ -174,21 +196,82 @@
</ion-card> </ion-card>
</ion-list> </ion-list>
<div class="empty-state" *ngIf="!loading && !errorMsg && horariosFiltrados.length === 0"> <div class="empty-state" *ngIf="!loading && !errorMsg && horariosFiltrados.length === 0 && viewMode !== 'plan'">
<p>No se encontraron horarios para la selección actual.</p> <p>No se encontraron horarios para la selección actual.</p>
</div> </div>
<!-- Vista: Mi Carrera (Plan de Estudios) -->
<div *ngIf="viewMode === 'plan'" class="ion-padding content-plan">
<ion-card class="carrera-selector-card">
<ion-card-content>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span class="selector-title" style="margin: 0;">Plan de Estudios / Recorrido</span>
<ion-button fill="clear" size="small" (click)="forzarRespaldo()" style="margin: 0;">
<ion-icon name="save-outline" slot="icon-only" color="primary"></ion-icon>
</ion-button>
</div>
<p class="selector-desc">Selecciona tu carrera para guardar tu avance y ver el recorrido sugerido, toca materias
para marcarlas como aprobadas o en curso.</p>
<ion-item lines="none" class="carrera-item">
<ion-icon name="school-outline" slot="start" color="primary"></ion-icon>
<ion-select placeholder="Elegí una carrera" [(ngModel)]="carreraSeleccionada" interface="popover">
<ion-select-option value="tecnologias">Lic. en Tecnologías Digitales</ion-select-option>
<ion-select-option value="datos">Lic. en Ciencia de Datos</ion-select-option>
<ion-select-option value="videojuegos">Lic. en Des. de Videojuegos</ion-select-option>
</ion-select>
</ion-item>
</ion-card-content>
</ion-card>
<div *ngIf="carreraSeleccionada" class="tabla-recorrido-container fade-in">
<table class="recorrido-table">
<thead>
<tr>
<th>1° Cuat.</th>
<th>2° Cuat.</th>
<th>3° Cuat.</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of getRecorridoData()">
<td>
<div class="materia-cell" [class.aprobada]="isAprobada(row.c1.name)"
[class.encurso]="isEnCurso(row.c1.name)" [class.optativa-cell]="row.c1.isOptativa"
(click)="handleCellClick(row.c1)">
{{ row.c1.name }}
</div>
</td>
<td>
<div class="materia-cell" [class.aprobada]="isAprobada(row.c2.name)"
[class.encurso]="isEnCurso(row.c2.name)" [class.optativa-cell]="row.c2.isOptativa"
(click)="handleCellClick(row.c2)">
{{ row.c2.name }}
</div>
</td>
<td>
<div class="materia-cell" [class.aprobada]="isAprobada(row.c3.name)"
[class.encurso]="isEnCurso(row.c3.name)" [class.optativa-cell]="row.c3.isOptativa"
(click)="handleCellClick(row.c3)">
{{ row.c3.name }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ion-content> </ion-content>
<ion-footer> <ion-footer class="ion-no-border">
<ion-toolbar color="primary" class="ion-no-padding"> <ion-toolbar class="footer-toolbar">
<div class="footer-content" <div class="modern-footer-btn" (click)="openMap()" title="Abrir en Mapas">
(click)="openLink('https://www.google.com/maps/place/UdelaCiudad/data=!4m2!3m1!1s0x0:0x6dab35ab5ab98245?sa=X&ved=1t:2428&ictx=111')" <ion-icon name="location" class="location-icon"></ion-icon>
style="cursor: pointer;" title="Mostrar en el mapa"> <div class="footer-text-column">
<div class="footer-text"> <span class="footer-title">U de la Ciudad</span>
<small><ion-icon name="location-outline" style="vertical-align: bottom;"></ion-icon> Tte. Gral. Juan Domingo <span class="footer-address">Tte. Gral. J.D. Perón 802 • CABA</span>
Perón 802 (C1038AAR)<br />CABA - Argentina</small>
</div> </div>
<ion-icon name="navigate-circle" class="nav-icon"></ion-icon>
</div> </div>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>
+178 -8
View File
@@ -47,17 +47,61 @@
} }
// Footer replica styles // Footer replica styles
.footer-content { .footer-toolbar {
--background: var(--ion-background-color);
--border-width: 0;
padding: 8px 14px;
padding-bottom: calc(8px + var(--ion-safe-area-bottom, 0px));
}
.modern-footer-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary));
padding: 10px; border-radius: 16px;
padding: 12px 16px;
.footer-text {
color: white; color: white;
font-size: 13px; box-shadow: 0 4px 15px rgba(var(--ion-color-primary-rgb), 0.3);
line-height: 1.2; transition: transform 0.2s, box-shadow 0.2s;
text-align: center; cursor: pointer;
&:active {
transform: scale(0.97);
box-shadow: 0 2px 8px rgba(var(--ion-color-primary-rgb), 0.2);
}
.location-icon {
font-size: 24px;
margin-right: 12px;
background: rgba(255, 255, 255, 0.2);
padding: 8px;
border-radius: 12px;
flex-shrink: 0;
}
.footer-text-column {
flex: 1;
display: flex;
flex-direction: column;
.footer-title {
font-size: 14px;
font-weight: 700;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.footer-address {
font-size: 12px;
opacity: 0.9;
}
}
.nav-icon {
font-size: 28px;
opacity: 0.95;
margin-left: 10px;
flex-shrink: 0;
} }
} }
@@ -345,3 +389,129 @@ ion-card.subject-card {
ion-icon { font-size: 1rem; flex-shrink: 0; } ion-icon { font-size: 1rem; flex-shrink: 0; }
} }
} }
// ─── Recorrido Académico (Plan de Estudios) ───────────────────────────
/* --- Vista: Mi Carrera (Plan de Estudios) --- */
.content-plan {
background: var(--ion-color-light);
}
.carrera-selector-card {
margin: 10px 0 20px 0;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
ion-card-content {
padding: 16px;
}
.selector-title {
display: block;
font-size: 1.15rem;
font-weight: 700;
color: var(--ion-color-dark);
margin-bottom: 6px;
}
.selector-desc {
font-size: 0.88rem;
color: var(--ion-color-medium);
margin-bottom: 16px;
line-height: 1.4;
}
.carrera-item {
--background: var(--ion-color-light);
border-radius: 8px;
--padding-start: 10px;
--inner-padding-end: 10px;
ion-select {
font-weight: 600;
color: var(--ion-color-primary);
width: 100%;
}
}
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.recorrido-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
th {
background: var(--ion-color-primary);
color: white;
padding: 8px 4px;
text-align: center;
font-weight: 600;
border-radius: 4px 4px 0 0;
}
td {
padding: 4px;
vertical-align: top;
width: 33.33%;
}
.materia-cell {
padding: 8px 4px;
height: 100%;
min-height: 60px;
background: var(--ion-color-light);
border-radius: 6px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid transparent;
&:active {
transform: scale(0.95);
}
&.aprobada {
background: #682cad;
color: white;
font-weight: 600;
box-shadow: 0 2px 6px rgba(104, 44, 173, 0.3);
border-color: rgba(0,0,0,0.1);
}
&.encurso {
background: rgba(var(--ion-color-secondary-rgb), 0.15); // Subtono Cyan/Celeste
color: var(--ion-color-secondary);
font-weight: 600;
border: 1.5px solid var(--ion-color-secondary);
}
&.optativa-cell:not(.aprobada):not(.encurso) {
border: 1.5px dashed var(--ion-color-medium);
color: var(--ion-color-primary);
background: transparent;
font-weight: 600;
}
}
}
.instruccion-marcar {
text-align: center;
color: var(--ion-color-medium);
margin-top: 12px;
font-style: italic;
}␍
+358 -14
View File
@@ -9,7 +9,7 @@ import { Haptics, ImpactStyle } from '@capacitor/haptics';
import { Share } from '@capacitor/share'; import { Share } from '@capacitor/share';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { CredencialModalComponent } from '../components/credencial-modal/credencial-modal.component'; import { CredencialModalComponent } from '../components/credencial-modal/credencial-modal.component';
import { CsvStorageService } from '../services/csv-storage.service'; import { LocalStorageService } from '../services/local-storage.service';
import { EventosService } from '../services/eventos.service'; import { EventosService } from '../services/eventos.service';
import { EventoModalComponent } from '../components/evento-modal/evento-modal.component'; import { EventoModalComponent } from '../components/evento-modal/evento-modal.component';
import { AgendaModalComponent } from '../components/agenda-modal/agenda-modal.component'; import { AgendaModalComponent } from '../components/agenda-modal/agenda-modal.component';
@@ -33,7 +33,7 @@ export class HomePage implements OnInit {
horariosFiltrados: ClaseHorario[] = []; horariosFiltrados: ClaseHorario[] = [];
favoritos: string[] = []; favoritos: string[] = [];
viewMode: 'favoritos' | 'todas' = 'todas'; viewMode: 'favoritos' | 'todas' | 'plan' = 'todas';
searchText: string = ''; searchText: string = '';
highlightedCodigo: string | null = null; highlightedCodigo: string | null = null;
@@ -46,6 +46,25 @@ export class HomePage implements OnInit {
private countdownTimer: any = null; private countdownTimer: any = null;
eventos: EventoAcademico[] = []; eventos: EventoAcademico[] = [];
aprobadas: string[] = [];
encurso: string[] = [];
optativasSeleccionadas: { slot: string, materia: string }[] = [];
opcionesOptativas: string[] = [
'Desarrollo en Dispositivos Móviles (ASIG00130)',
'Diseño y Producción de Videojuegos (ASIG00133)',
'Herramientas Jurídicas en la Industria Digital (ASIG00132)',
'IOT - Internet de las cosas (ASIG00189)',
'Análisis de datos II: Sistemas expertos y redes de conocimiento (ASIG00131)',
'Gestión del Talento Humano en la Industria digital (ASIG00205)',
'Business Intelligence (ASIG00201)',
'Bases de Datos (ASIG00199)',
'Marketing Digital (ASIG00215)',
'Gestión de Redes Informáticas (ASIG00296)',
'Transformación Digital en las Organizaciones (ASIG00297)'
];
carreraSeleccionada: string = 'tecnologias';
// Mapa de días en español al getDay() de JS (0=Dom) // Mapa de días en español al getDay() de JS (0=Dom)
@@ -60,7 +79,7 @@ export class HomePage implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private modalCtrl: ModalController, private modalCtrl: ModalController,
private alertCtrl: AlertController, private alertCtrl: AlertController,
private csvStorageService: CsvStorageService, private localStorageService: LocalStorageService,
private actionSheetCtrl: ActionSheetController, private actionSheetCtrl: ActionSheetController,
private eventosService: EventosService private eventosService: EventosService
) { } ) { }
@@ -92,10 +111,13 @@ export class HomePage implements OnInit {
this.loading = true; this.loading = true;
// 0. Intentar cargar los datos de usuario desde el archivo CSV seguro // 0. Intentar cargar los datos de usuario desde Preferences (incluye parche de migración)
const userData = await this.csvStorageService.loadUserDataFromCsv(); const userData = await this.localStorageService.loadData();
if (userData) { if (userData) {
this.favoritos = userData.favoritos; this.favoritos = userData.favoritos;
this.aprobadas = userData.aprobadas || [];
this.encurso = userData.encurso || [];
this.optativasSeleccionadas = userData.optativas || [];
// Sincronizar eventos cargados desde el CSV con el servicio de eventos // Sincronizar eventos cargados desde el CSV con el servicio de eventos
for (const ev of userData.eventos) { for (const ev of userData.eventos) {
await this.eventosService.saveEvento(ev); await this.eventosService.saveEvento(ev);
@@ -151,8 +173,8 @@ export class HomePage implements OnInit {
const favItems = this.horarios.filter(h => this.favoritos.includes(h.codigo)); const favItems = this.horarios.filter(h => this.favoritos.includes(h.codigo));
this.dataService.scheduleClassReminders(favItems); this.dataService.scheduleClassReminders(favItems);
// Auto-reparación/Actualización del CSV de respaldo // Auto-reparación/Actualización del Respaldo
this.csvStorageService.saveUserDataToCsv(this.favoritos, this.eventos); this.guardarProgreso();
if (event) event.target.complete(); if (event) event.target.complete();
}, },
@@ -191,9 +213,19 @@ export class HomePage implements OnInit {
{ {
text: 'Buscar Respaldo', text: 'Buscar Respaldo',
handler: async () => { handler: async () => {
const imported = await this.csvStorageService.importFavoritesFromFilePicker(); const imported = await this.localStorageService.importBackup();
if (imported && imported.length > 0) { if (imported && (imported.favoritos.length > 0 || imported.eventos.length > 0 || imported.aprobadas?.length > 0 || imported.optativas?.length > 0 || imported.encurso?.length > 0)) {
this.favoritos = imported; this.favoritos = imported.favoritos;
this.aprobadas = imported.aprobadas || [];
this.encurso = imported.encurso || [];
this.optativasSeleccionadas = imported.optativas || [];
// Sincronizar eventos cargados desde el CSV con el servicio de eventos
for (const ev of imported.eventos) {
await this.eventosService.saveEvento(ev);
}
this.eventos = await this.eventosService.loadEventos();
this.viewMode = 'favoritos'; this.viewMode = 'favoritos';
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) }); await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
this.filterHorarios(); this.filterHorarios();
@@ -444,7 +476,7 @@ export class HomePage implements OnInit {
this.favoritos.push(item.codigo); this.favoritos.push(item.codigo);
} }
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) }); await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
await this.csvStorageService.saveUserDataToCsv(this.favoritos, this.eventos); this.guardarProgreso();
this.filterHorarios(); this.filterHorarios();
} }
} }
@@ -492,6 +524,29 @@ export class HomePage implements OnInit {
window.open(url, '_blank', 'noopener'); window.open(url, '_blank', 'noopener');
} }
openMap() {
const address = 'Tte. Gral. Juan Domingo Perón 802, C1063, Cdad. Autónoma de Buenos Aires';
const encodedAddress = encodeURIComponent(address);
// Si estamos en un dispositivo móvil, preferimos los esquemas nativos para invocar al OS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isAndroid = /android/i.test(navigator.userAgent);
let url = '';
if (isAndroid) {
// En Android, geo:0,0?q= fuerza al sistema a abrir el selector de mapas (Ej: Waze, Maps)
url = `geo:0,0?q=${encodedAddress}`;
} else if (isIOS) {
// En iOS, maps:// invoca Apple Maps (en algunos casos permite elegir)
url = `maps://?q=${encodedAddress}`;
} else {
// Fallback para Web/Desktop
url = `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
}
window.open(url, '_system');
}
async openCredencial() { async openCredencial() {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
component: CredencialModalComponent component: CredencialModalComponent
@@ -597,7 +652,7 @@ export class HomePage implements OnInit {
const { data, role } = await modal.onWillDismiss(); const { data, role } = await modal.onWillDismiss();
if (role === 'saved' || role === 'deleted') { if (role === 'saved' || role === 'deleted') {
this.eventos = await this.eventosService.loadEventos(); this.eventos = await this.eventosService.loadEventos();
await this.csvStorageService.saveUserDataToCsv(this.favoritos, this.eventos); this.guardarProgreso();
} }
} }
@@ -619,8 +674,8 @@ export class HomePage implements OnInit {
// Y aseguramos que la próxima clase y los badges se actualicen // Y aseguramos que la próxima clase y los badges se actualicen
this.actualizarProximaClase(); this.actualizarProximaClase();
// Guardamos en CSV por si hubo cambios // Guardamos progreso por si hubo cambios
await this.csvStorageService.saveUserDataToCsv(this.favoritos, this.eventos); this.guardarProgreso();
} }
@@ -634,5 +689,294 @@ export class HomePage implements OnInit {
const diff = new Date(fechaIso).getTime() - new Date().getTime(); const diff = new Date(fechaIso).getTime() - new Date().getTime();
return diff > 0 && diff < (1000 * 60 * 60 * 48); // Menos de 48 horas return diff > 0 && diff < (1000 * 60 * 60 * 48); // Menos de 48 horas
} }
// --- Recorrido Académico ---
getRecorridoData() {
const getSlot = (slotId: string, label: string) => {
const selected = this.optativasSeleccionadas.find(o => o.slot === slotId);
return { id: slotId, name: selected ? selected.materia : label, isOptativa: true };
};
const mat = (name: string) => ({ id: null, name: name, isOptativa: false });
if (this.carreraSeleccionada === 'tecnologias') {
return [
{ c1: mat('Gestión de Sistemas de Información'), c2: mat('Administración de Negocios Digitales'), c3: mat('Seguridad de la Información') },
{ c1: mat('Análisis de Datos I'), c2: mat('Sistemas Digitales'), c3: mat('Programación de Vanguardia') },
{ c1: mat('Taller: Proyectos Digitales'), c2: mat('Taller: Emprendedurismo en Innovación Digital'), c3: mat('Taller: Control de calidad de Software') },
{ c1: getSlot('tec_opt1', 'Optativa 1'), c2: getSlot('tec_opt2', 'Optativa 2'), c3: getSlot('tec_opt3', 'Optativa 3') }
];
} else if (this.carreraSeleccionada === 'datos') {
return [
{ c1: mat('Materia Obligatoria 1'), c2: mat('Materia Obligatoria 4'), c3: mat('Materia Obligatoria 7') },
{ c1: mat('Materia Obligatoria 2'), c2: mat('Materia Obligatoria 5'), c3: mat('Materia Obligatoria 8') },
{ c1: mat('Materia Obligatoria 3'), c2: mat('Materia Obligatoria 6'), c3: mat('Materia Obligatoria 9') },
{ c1: getSlot('dat_opt1', 'Optativa 1'), c2: getSlot('dat_opt2', 'Optativa 2'), c3: getSlot('dat_opt3', 'Optativa 3') }
];
} else {
return [
{ c1: mat('Materia Obligatoria 1'), c2: mat('Materia Obligatoria 4'), c3: mat('Materia Obligatoria 7') },
{ c1: mat('Materia Obligatoria 2'), c2: mat('Materia Obligatoria 5'), c3: mat('Materia Obligatoria 8') },
{ c1: mat('Materia Obligatoria 3'), c2: mat('Materia Obligatoria 6'), c3: mat('Materia Obligatoria 9') },
{ c1: getSlot('vid_opt1', 'Optativa 1'), c2: getSlot('vid_opt2', 'Optativa 2'), c3: getSlot('vid_opt3', 'Optativa 3') }
];
}
}
// --- Manejo de Swipe para Navegación ---
onSwipe(event: any) {
const modes: ('favoritos' | 'todas' | 'plan')[] = ['favoritos', 'todas', 'plan'];
const currentIndex = modes.indexOf(this.viewMode);
if (event.direction === 2) {
// Swipe Izquierda (Siguiente Tab)
if (currentIndex < modes.length - 1) {
this.viewMode = modes[currentIndex + 1];
this.onViewModeChange();
}
} else if (event.direction === 4) {
// Swipe Derecha (Tab Anterior)
if (currentIndex > 0) {
this.viewMode = modes[currentIndex - 1];
this.onViewModeChange();
}
}
}
isAprobada(materia: string): boolean {
return this.aprobadas.includes(materia);
}
isEnCurso(materia: string): boolean {
return this.encurso.includes(materia);
}
async handleCellClick(cell: { id: string | null, name: string, isOptativa: boolean }) {
if (cell.isOptativa && !this.optativasSeleccionadas.find(o => o.slot === cell.id)) {
await this.abrirSelectorOptativas(cell.id!);
return;
}
const isAppr = this.isAprobada(cell.name);
const isCour = this.isEnCurso(cell.name);
const buttons: any[] = [
{
text: isAppr ? 'Quitar Aprobado' : 'Marcar como Aprobada',
icon: isAppr ? 'close-circle-outline' : 'checkmark-done-circle-outline',
handler: () => {
this.setEstadoMateria(cell.name, 'aprobada');
}
},
{
text: isCour ? 'Quitar En Curso' : 'Marcar En Curso',
icon: isCour ? 'close-circle-outline' : 'book-outline',
handler: () => {
this.setEstadoMateria(cell.name, 'encurso');
}
}
];
if (cell.isOptativa) {
buttons.push({
text: 'Cambiar de Materia Optativa',
icon: 'swap-horizontal-outline',
handler: () => {
this.abrirSelectorOptativas(cell.id!);
}
});
buttons.push({
text: 'Borrar Selección de Optativa',
role: 'destructive',
icon: 'trash-outline',
handler: () => {
this.aprobadas = this.aprobadas.filter(m => m !== cell.name);
this.encurso = this.encurso.filter(m => m !== cell.name);
this.optativasSeleccionadas = this.optativasSeleccionadas.filter(o => o.slot !== cell.id);
this.guardarProgreso();
}
});
} else {
buttons.push({
text: 'Reiniciar Estado (Pendiente)',
role: 'destructive',
icon: 'refresh-outline',
handler: () => {
this.aprobadas = this.aprobadas.filter(m => m !== cell.name);
this.encurso = this.encurso.filter(m => m !== cell.name);
this.guardarProgreso();
}
});
}
buttons.push({
text: 'Cancelar',
icon: 'close',
role: 'cancel'
});
const actionSheet = await this.actionSheetCtrl.create({
header: cell.name,
subHeader: cell.isOptativa ? 'Gestión de Materia Optativa' : 'Gestión de Materia',
buttons: buttons
});
await actionSheet.present();
}
async abrirSelectorOptativas(slotId: string) {
const radios = this.opcionesOptativas.map(opt => ({
name: 'optativa',
type: 'radio' as any,
label: opt,
value: opt,
checked: false
}));
const alert = await this.alertCtrl.create({
header: 'Elegir Materia Optativa',
inputs: radios,
buttons: [
{ text: 'Cancelar', role: 'cancel' },
{
text: 'Seleccionar',
handler: (selectedValue) => {
if (selectedValue) {
const existingIndex = this.optativasSeleccionadas.findIndex(o => o.slot === slotId);
if (existingIndex > -1) {
const oldName = this.optativasSeleccionadas[existingIndex].materia;
this.aprobadas = this.aprobadas.filter(m => m !== oldName);
this.encurso = this.encurso.filter(m => m !== oldName);
this.optativasSeleccionadas[existingIndex].materia = selectedValue;
} else {
this.optativasSeleccionadas.push({ slot: slotId, materia: selectedValue });
}
this.guardarProgreso();
}
}
}
]
});
await alert.present();
}
setEstadoMateria(materia: string, estado: 'aprobada' | 'encurso') {
if (estado === 'aprobada') {
if (this.isAprobada(materia)) {
this.aprobadas = this.aprobadas.filter(m => m !== materia);
} else {
this.aprobadas.push(materia);
this.encurso = this.encurso.filter(m => m !== materia); // Quita de en curso si s aprueba
}
} else if (estado === 'encurso') {
if (this.isEnCurso(materia)) {
this.encurso = this.encurso.filter(m => m !== materia);
} else {
this.encurso.push(materia);
this.aprobadas = this.aprobadas.filter(m => m !== materia); // Quita aprobada si se pone en curso
}
}
this.guardarProgreso();
}
guardarProgreso() {
this.localStorageService.saveData({
favoritos: this.favoritos,
eventos: this.eventos,
aprobadas: this.aprobadas,
optativas: this.optativasSeleccionadas,
encurso: this.encurso
});
}
async forzarRespaldo() {
this.guardarProgreso();
const data = {
favoritos: this.favoritos,
eventos: this.eventos,
aprobadas: this.aprobadas,
optativas: this.optativasSeleccionadas,
encurso: this.encurso
};
const actionSheet = await this.actionSheetCtrl.create({
header: 'Guardar Respaldo',
subHeader: '¿A dónde querés exportar tus materias guardadas?',
buttons: [
{
text: 'Compartir (Drive, WhatsApp, Mail...)',
icon: 'share-social-outline',
handler: async () => {
await this.ejecutarExportacion(data, false);
}
},
{
text: 'Descargar Offline (A la carpeta Descargas)',
icon: 'download-outline',
handler: async () => {
await this.ejecutarExportacion(data, true);
}
},
{
text: 'Cancelar',
icon: 'close',
role: 'cancel'
}
]
});
await actionSheet.present();
}
private async ejecutarExportacion(data: any, localMode: boolean) {
const ok = await this.localStorageService.exportBackup(data, localMode);
if (ok) {
const toast = await this.toastCtrl.create({
message: localMode ? 'Archivo descargado en tu teléfono.' : 'Respaldo emitido correctamente.',
duration: 3500,
color: 'success',
icon: 'save'
});
toast.present();
await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => {});
}
}
async importarRespaldoManual() {
const imported = await this.localStorageService.importBackup();
if (imported && (imported.favoritos.length > 0 || imported.eventos.length > 0 || imported.aprobadas?.length > 0 || imported.optativas?.length > 0 || imported.encurso?.length > 0)) {
this.favoritos = imported.favoritos;
this.aprobadas = imported.aprobadas || [];
this.encurso = imported.encurso || [];
this.optativasSeleccionadas = imported.optativas || [];
for (const ev of imported.eventos) {
await this.eventosService.saveEvento(ev);
}
this.eventos = await this.eventosService.loadEventos();
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
this.guardarProgreso();
this.filterHorarios();
const toast = await this.toastCtrl.create({
message: '¡Respaldo importado correctamente!',
duration: 3000,
color: 'success',
icon: 'checkmark-circle'
});
toast.present();
await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => {});
} else {
const toast = await this.toastCtrl.create({
message: 'No se procesó ningún archivo válido.',
duration: 2500,
color: 'warning',
icon: 'alert-circle'
});
toast.present();
}
}
} }
-165
View File
@@ -1,165 +0,0 @@
import { Injectable } from '@angular/core';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import { EventoAcademico } from './data';
@Injectable({
providedIn: 'root'
})
export class CsvStorageService {
private readonly FILE_NAME = 'aulas_unicaba_datos.csv';
constructor() {}
private async ensurePermissions(): Promise<boolean> {
try {
let status = await Filesystem.checkPermissions();
if (status.publicStorage !== 'granted') {
status = await Filesystem.requestPermissions();
}
return status.publicStorage === 'granted';
} catch (e) {
return true;
}
}
async saveUserDataToCsv(favoritos: string[], eventos: EventoAcademico[]) {
try {
const hasPermission = await this.ensurePermissions();
if (!hasPermission) {
console.warn('Permisos denegados para escribir en Documents');
return;
}
let csvContent = 'tipo,materia,id_o_valor,extra1,extra2,extra3,extra4\n';
// Filas de favoritos
favoritos.forEach(codigo => {
csvContent += `favorito,${codigo},,,,\n`;
});
// Filas de eventos
eventos.forEach(ev => {
// Formato: evento,CODIGO,ID,TIPO,FECHA,AULA,LINK,NOTAS
// Para simplificar el CSV sin escapar comas en notas, reemplazamos comas por punto y coma
const notasEscaped = (ev.notas || '').replace(/,/g, ';');
csvContent += `evento,${ev.materiaCodigo},${ev.id},${ev.tipo},${ev.fecha},${ev.aula || ''},${ev.link || ''},${notasEscaped}\n`;
});
// Intentamos borrar si existe
try {
await Filesystem.deleteFile({
path: this.FILE_NAME,
directory: Directory.Documents
});
} catch (e) {}
await Filesystem.writeFile({
path: this.FILE_NAME,
data: csvContent,
directory: Directory.Documents,
encoding: Encoding.UTF8
});
console.log('CSV con datos de usuario guardado en Documents');
} catch (e) {
console.error('Error al guardar el CSV:', e);
}
}
async loadUserDataFromCsv(): Promise<{ favoritos: string[], eventos: EventoAcademico[] } | null> {
try {
const hasPermission = await this.ensurePermissions();
if (!hasPermission) return null;
const contents = await Filesystem.readFile({
path: this.FILE_NAME,
directory: Directory.Documents,
encoding: Encoding.UTF8
});
if (!contents.data) return null;
const lines = contents.data.toString().split('\n');
const favs: string[] = [];
const evs: EventoAcademico[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(',');
const tipo = parts[0];
if (tipo === 'favorito') {
if (parts.length > 1 && parts[1]) {
favs.push(parts[1]);
}
} else if (tipo === 'evento') {
// evento,materia,id,tipo_ev,fecha,aula,link,notas
if (parts.length >= 5) {
evs.push({
materiaCodigo: parts[1],
id: parts[2],
tipo: parts[3] as any,
fecha: parts[4],
aula: parts[5] || '',
link: parts[6] || '',
notas: (parts[7] || '').replace(/;/g, ',')
});
}
}
}
return { favoritos: favs, eventos: evs };
} catch (e) {
return null;
}
}
async importFavoritesFromFilePicker(): Promise<string[] | null> {
try {
const result = await FilePicker.pickFiles({
types: ['text/csv', 'text/comma-separated-values', '.csv'],
readData: true
});
const file = result.files[0];
if (!file || !file.data) return null;
const text = decodeURIComponent(escape(window.atob(file.data)));
const lines = text.split('\n');
const favs: string[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(',');
const tipo = parts[0];
if (tipo === 'favorito') {
if (parts.length > 1 && parts[1]) {
favs.push(parts[1]);
}
}
// Nota: para el Picker manual solo recuperamos favoritos por ahora para simplicidad,
// o podríamos reusar la lógica de loadUserDataFromCsv si el CSV es idéntico.
}
if (favs.length > 0) {
await this.saveUserDataToCsv(favs, []);
return favs;
}
return null;
} catch(e) {
console.warn('File picker cancelado o falló', e);
return null;
}
}
}
+223
View File
@@ -0,0 +1,223 @@
import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import { Share } from '@capacitor/share';
import { Capacitor } from '@capacitor/core';
import { EventoAcademico } from './data';
export interface AppData {
favoritos: string[];
eventos: EventoAcademico[];
aprobadas: string[];
optativas: {slot: string, materia: string}[];
encurso: string[];
}
@Injectable({
providedIn: 'root'
})
export class LocalStorageService {
private readonly PREF_KEY = 'aulas_unicaba_data';
private readonly BACKUP_PREFIX = 'Aulas_UNICABA_Respaldo_';
constructor() {}
// --- INTERNAL PREFERENCES (ROCK SOLID OFFLINE) ---
async saveData(data: AppData): Promise<void> {
try {
await Preferences.set({
key: this.PREF_KEY,
value: JSON.stringify(data)
});
} catch (e) {
console.error('Error saving to Preferences', e);
}
}
async loadData(): Promise<AppData | null> {
try {
let { value } = await Preferences.get({ key: this.PREF_KEY });
if (!value) {
// Intento de migración la primera vez si el usuario viene de versiones viejas con CSV
await this.migrateLegacyCsvIfNeeded();
const res = await Preferences.get({ key: this.PREF_KEY });
value = res.value;
}
if (value) {
const parsed = JSON.parse(value);
return {
favoritos: parsed.favoritos || [],
eventos: parsed.eventos || [],
aprobadas: parsed.aprobadas || [],
optativas: parsed.optativas || [],
encurso: parsed.encurso || []
};
}
return null;
} catch (e) {
console.error('Error loading from Preferences', e);
return null;
}
}
async exportBackup(data: AppData, localDownload: boolean = false): Promise<boolean> {
try {
const backupData = JSON.stringify(data, null, 2);
const safeDate = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16);
const fileName = `${this.BACKUP_PREFIX}${safeDate}.json`;
const platform = Capacitor.getPlatform();
if (localDownload && (platform === 'android' || platform === 'web')) {
// En Android y Web, forzamos la descarga extrayéndolo físicamente a Descargas
const blob = new Blob([backupData], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
// iOS: El Share sheet sí incluye la opción "Guardar en Archivos" por defecto
const writeResult = await Filesystem.writeFile({
path: fileName,
data: backupData,
directory: Directory.Cache,
encoding: Encoding.UTF8
});
await Share.share({
title: 'Respaldo Aulas UNICABA',
text: 'Este archivo contiene el respaldo de tus materias.',
url: writeResult.uri,
dialogTitle: 'Guardar Respaldo en Archivos'
});
}
return true;
} catch (e) {
console.error('Error exporting backup', e);
return false;
}
}
async importBackup(): Promise<AppData | null> {
try {
// 1. Let the user explicitly pick the file from their OS sin restriccion
const result = await FilePicker.pickFiles({
readData: true
});
const file = result.files[0];
if (!file || !file.data) return null;
// 2. Decode the Base64 data from the picker
const text = decodeURIComponent(escape(window.atob(file.data)));
// Intentamos cargar JSON puro primero
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
const importedData: AppData = {
favoritos: parsed.favoritos || [],
eventos: parsed.eventos || [],
aprobadas: parsed.aprobadas || [],
optativas: parsed.optativas || [],
encurso: parsed.encurso || []
};
await this.saveData(importedData);
return importedData;
}
} catch (jsonErr) {
// Fallback: intentar parsear el legado .csv
const legacyParsed = await this.parseLegacyCsv(text, true);
return legacyParsed;
}
// Retornar en caso de que sea un json malformado que no falle el JSON.parse
return null;
} catch(e) {
console.warn('File picker cancelado o error al importar', e);
return null;
}
}
// --- MIGRATION LOGIC ---
private async migrateLegacyCsvIfNeeded(): Promise<void> {
try {
const contents = await Filesystem.readFile({
path: 'aulas_unicaba_datos.csv',
directory: Directory.Documents,
encoding: Encoding.UTF8
});
if (contents && contents.data) {
const legacyData = await this.parseLegacyCsv(contents.data.toString(), true);
if (legacyData) {
console.log('Migración Exitosa. Datos viejos capturados en el nuevo Preference API.');
}
}
} catch (e) {
// Ignore, user is fresh
}
}
private async parseLegacyCsv(text: string, saveAfter: boolean): Promise<AppData | null> {
try {
const lines = text.split('\n');
const favs: string[] = [];
const evs: EventoAcademico[] = [];
const aprobadas: string[] = [];
const encurso: string[] = [];
const optativasMap: {slot: string, materia: string}[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(',');
if (parts.length < 2) continue;
const tipo = parts[0];
if (tipo === 'favorito') {
favs.push(parts[1]);
} else if (tipo === 'aprobada') {
aprobadas.push(parts[1]);
} else if (tipo === 'en_curso') {
encurso.push(parts[1]);
} else if (tipo === 'optativa' && parts[2]) {
optativasMap.push({ slot: parts[1], materia: parts[2] });
} else if (tipo === 'evento' && parts.length >= 5) {
evs.push({
materiaCodigo: parts[1],
id: parts[2],
tipo: parts[3] as any,
fecha: parts[4],
aula: parts[5] || '',
link: parts[6] || '',
notas: (parts[7] || '').replace(/;/g, ',')
});
}
}
const importedData: AppData = {
favoritos: favs, eventos: evs, aprobadas: aprobadas, optativas: optativasMap, encurso: encurso
};
if (saveAfter) {
await this.saveData(importedData);
}
return importedData;
} catch (e) {
console.error('Error parsing legacy CSV', e);
return null;
}
}
}