diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 46875a5..657ea6e 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,6 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-community-file-opener') implementation project(':capacitor-community-screen-brightness') implementation project(':capacitor-app') implementation project(':capacitor-clipboard') diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 26f0d84..5d03e61 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-community-file-opener' +project(':capacitor-community-file-opener').projectDir = new File('../node_modules/@capacitor-community/file-opener/android') + include ':capacitor-community-screen-brightness' project(':capacitor-community-screen-brightness').projectDir = new File('../node_modules/@capacitor-community/screen-brightness/android') diff --git a/package-lock.json b/package-lock.json index b1526cf..cc78145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aulas-unicaba", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aulas-unicaba", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@angular/animations": "~20.3.18", "@angular/common": "~20.3.18", @@ -16,6 +16,7 @@ "@angular/platform-browser": "~20.3.18", "@angular/platform-browser-dynamic": "~20.3.18", "@angular/router": "~20.3.18", + "@capacitor-community/file-opener": "^8.0.0", "@capacitor-community/screen-brightness": "^8.0.0", "@capacitor/android": "^8.2.0", "@capacitor/app": "8.0.1", @@ -601,7 +602,6 @@ "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" @@ -631,7 +631,6 @@ "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" }, @@ -781,7 +780,6 @@ "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" }, @@ -798,7 +796,6 @@ "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" }, @@ -812,7 +809,6 @@ "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", @@ -845,7 +841,6 @@ "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" }, @@ -871,7 +866,6 @@ "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" }, @@ -900,7 +894,6 @@ "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" }, @@ -941,7 +934,6 @@ "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" }, @@ -986,7 +978,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2619,6 +2610,19 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor-community/file-opener": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@capacitor-community/file-opener/-/file-opener-8.0.0.tgz", + "integrity": "sha512-kbVNxt4G45RHHeOqrE1Z0nT5iOOzmZ3WwQytpUtaIPzDTlmZxVEA3nJYEiOfGFp6msZO6QWHK3k0Qbv7hnBx+w==", + "license": "MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor-community/screen-brightness": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@capacitor-community/screen-brightness/-/screen-brightness-8.0.0.tgz", @@ -2836,7 +2840,6 @@ "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" } @@ -3968,7 +3971,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -7091,7 +7093,6 @@ "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -7232,7 +7233,6 @@ "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", @@ -7340,7 +7340,6 @@ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7413,7 +7412,6 @@ "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", @@ -7698,7 +7696,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7774,7 +7771,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8473,7 +8469,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10133,7 +10128,6 @@ "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", @@ -10626,7 +10620,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11415,7 +11408,6 @@ "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -12498,8 +12490,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jasmine-spec-reporter": { "version": "5.0.2", @@ -12548,7 +12539,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12689,7 +12679,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -13267,7 +13256,6 @@ "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", @@ -13412,7 +13400,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -15310,7 +15297,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16143,7 +16129,6 @@ "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" } @@ -16237,7 +16222,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17380,7 +17364,6 @@ "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", @@ -17582,8 +17565,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -17719,7 +17701,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17985,7 +17966,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -18120,7 +18100,6 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18223,7 +18202,6 @@ "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", @@ -19206,7 +19184,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19225,8 +19202,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/package.json b/package.json index da60931..ae8aad9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser": "~20.3.18", "@angular/platform-browser-dynamic": "~20.3.18", "@angular/router": "~20.3.18", + "@capacitor-community/file-opener": "^8.0.0", "@capacitor-community/screen-brightness": "^8.0.0", "@capacitor/android": "^8.2.0", "@capacitor/app": "8.0.1", diff --git a/src/app/components/info-modal/info-modal.component.html b/src/app/components/info-modal/info-modal.component.html index 56a0d71..6a2c8e8 100644 --- a/src/app/components/info-modal/info-modal.component.html +++ b/src/app/components/info-modal/info-modal.component.html @@ -10,6 +10,9 @@ Manual + + Beneficios + Privacidad @@ -30,9 +33,11 @@

1. Mi Cartelera (Horarios y Aulas)

-

Visualizá todas las materias organizadas por día. Usá la barra de búsqueda para filtrar rápido por nombre, aula o código.

+

Visualizá todas las materias organizadas por día. Usá la barra de búsqueda para filtrar rápido por nombre, + aula o código.

@@ -41,24 +46,29 @@

2. Mi Carrera (Seguimiento)

Accedé desde el botón inferior para ver tu plan de estudios.

3. Agenda y Exámenes

-

En el menú de cada materia (`...`) podés agendar Parciales, Finales o TPs. Verás insignias de colores avisándote la proximidad.

+

En el menú de cada materia (`...`) podés agendar Parciales, Finales o TPs. Verás insignias + de colores avisándote la proximidad.

4. Credencial Digital QR

-

Tocá el logo arriba a la izquierda para abrir tu credencial. La app subirá el brillo automáticamente para facilitar el escaneo.

+

Tocá el logo arriba a la izquierda para abrir tu credencial. La app subirá el brillo automáticamente para + facilitar el escaneo.

5. Mantené tus datos seguros

-

Usa Perfil > Guardar Respaldo para exportar un archivo CSV. Si cambias de teléfono, podés restaurarlo desde el mismo lugar.

+

Usa Perfil > Guardar Respaldo para exportar un archivo CSV. Si cambias de teléfono, podés + restaurarlo desde el mismo lugar.

@@ -73,10 +83,12 @@

Sin Intermediarios

-

Aulas UNICABA no tiene servidores propios. Cada vez que sincronizás la cartelera o tu credencial, la conexión viaja directo de tu teléfono al servidor de la Universidad.

+

Aulas UNICABA no tiene servidores propios. Cada vez que sincronizás la cartelera o tu credencial, la conexión + viaja directo de tu teléfono al servidor de la Universidad.

Tratamiento de Credenciales

-

Tus claves de Moodle o SIU Guaraní se guardan de forma encriptada en el almacenamiento seguro de tu dispositivo. Nunca salen de tu teléfono hacia terceros.

+

Tus claves de Moodle o SIU Guaraní se guardan de forma encriptada en el almacenamiento seguro de tu + dispositivo. Nunca salen de tu teléfono hacia terceros.

Analíticas y Rastreos

No usamos Google Analytics ni SDKs de seguimiento. No hay publicidad. Cero telemetría invasiva.

@@ -87,4 +99,56 @@
- + +
+
+ +

Guía de Beneficios

+

Aprovechá tu formación en UNICABA

+
+ +
+
+
+ +

1. SOY Estudiante (Transporte)

+
+

Obtené descuentos en pasajes de micro a todo el país a través del portal de Central de Pasajes.

+
+

¿Cómo activarlo?

+
    +
  1. Entra a Perfil y generá tu Certificado de Alumno Regular.
  2. +
  3. Ingresá a soyestudiante.com.ar.
  4. +
  5. Subí el PDF que guardaste en tu teléfono.
  6. +
  7. ¡Listo! Tu DNI quedará habilitado para descuentos automáticos.
  8. +
+
+
+ +
+
+ +

2. Google Students (IA y Nube)

+
+

Cargando la Constancia de Alumno Regular con tu cuenta personal te da acceso a herramientas + profesionales. +

+
+

Beneficios incluidos:

+
    +
  • Google Gemini PRO: Bonificación por un año de IA avanzada.
  • +
  • Google Drive: Almacenamiento ampliado (hasta 1TB según vigencia).
  • +
  • Meet Premium: Reuniones sin límite de tiempo y grabación directa.
  • +
+

* Para validar, usá tu mail institucional y subí el certificado si se solicita comprobante de + cursada.

+
+
+ + +
+
+ + \ No newline at end of file diff --git a/src/app/perfil/perfil.page.html b/src/app/perfil/perfil.page.html index 03ec65c..b54a6b9 100644 --- a/src/app/perfil/perfil.page.html +++ b/src/app/perfil/perfil.page.html @@ -70,6 +70,10 @@ DNI: {{ siuDni }} + + + Certificado de Alumno Regular + Cerrar sesión SIU @@ -167,6 +171,39 @@ + +
+

Beneficios del Estudiante

+

Aprovechá convenios exclusivos por ser parte de UNICABA.

+
+ + + + +
+ +
+ +

SOY Estudiante

+

Descuentos en pasajes de micro.

+ Activo +
+
+
+ + +
+ +
+ +

Google Students

+

Gemini PRO & Drive 1TB.

+ Bonificado +
+
+
+
+
diff --git a/src/app/perfil/perfil.page.scss b/src/app/perfil/perfil.page.scss index eb2873b..57111f3 100644 --- a/src/app/perfil/perfil.page.scss +++ b/src/app/perfil/perfil.page.scss @@ -189,65 +189,100 @@ } } -.info-section-container { - background: white; - border-radius: 20px; - margin-bottom: 20px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); - - .section-title { +.btn-cert { + margin-top: 12px; + --border-radius: 12px; + font-weight: 600; +} + +.section-header { + margin-top: 24px; + h3 { + margin: 0; + font-size: 1.2rem; font-weight: 700; - font-size: 1.1rem; color: var(--ion-color-dark); - margin: 0 0 15px 0; } - - .info-item { + p { + margin: 4px 0 0; font-size: 0.9rem; - line-height: 1.5; color: var(--ion-color-medium); - margin-bottom: 15px; - display: flex; - align-items: flex-start; - gap: 12px; - - ion-icon { - font-size: 1.4rem; - flex-shrink: 0; - } - - strong { - color: var(--ion-color-dark); - } - } - - .version-info { - margin-top: 20px; - border-top: 1px solid rgba(0, 0, 0, 0.05); - padding-top: 15px; - color: var(--ion-color-medium); - opacity: 0.6; } } -.error-text { - color: var(--ion-color-danger); - font-size: 0.85rem; - margin-top: 10px; - font-weight: 500; - text-align: center; +.benefits-row { + padding: 0 8px; + margin-top: -8px; +} + +.benefit-card { + margin: 8px; + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); + display: flex; + flex-direction: column; + height: calc(100% - 16px); + transition: transform 0.2s; + overflow: hidden; + + &:active { + transform: scale(0.96); + } + + .benefit-icon-container { + height: 80px; + display: flex; + align-items: center; + justify-content: center; + + &.soy { + background: linear-gradient(135deg, #f0f7ff 0%, #e0e7ff 100%); + ion-icon { font-size: 40px; color: #1a73e8; } + } + + &.google { + background: linear-gradient(135deg, #fff5f5 0%, #ffe0e0 100%); + ion-icon { font-size: 40px; } + } + } + + ion-card-content { + padding: 12px; + text-align: center; + + h4 { + margin: 0; + font-size: 0.95rem; + font-weight: 700; + color: var(--ion-color-dark); + } + p { + margin: 6px 0; + font-size: 0.75rem; + color: var(--ion-color-medium); + line-height: 1.2; + } + ion-badge { + font-size: 0.65rem; + padding: 4px 8px; + border-radius: 6px; + } + } } .security-info { + margin-top: 24px; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; opacity: 0.7; + ion-icon { font-size: 30px; } + p { font-size: 0.8rem; margin: 0; @@ -255,3 +290,20 @@ max-width: 280px; } } + +:host ::ng-deep .benefit-alert { + .alert-message { + text-align: left; + } + .benefit-modal-content { + b { color: var(--ion-color-primary); } + ol { + padding-left: 20px; + margin-top: 10px; + } + li { + margin-bottom: 8px; + font-size: 0.9rem; + } + } +} diff --git a/src/app/perfil/perfil.page.ts b/src/app/perfil/perfil.page.ts index 8370d00..961cf24 100644 --- a/src/app/perfil/perfil.page.ts +++ b/src/app/perfil/perfil.page.ts @@ -168,6 +168,50 @@ export class PerfilPage implements OnInit, OnDestroy { await alert.present(); } + // --- AUTOMATIZACIÓN DE CERTIFICADOS --- + + async generateCertificate() { + const creds = await this.authService.getSiuCredentials(); + if (!creds.dni || !creds.pass) { + this.showToast('Debe estar conectado al SIU para generar el certificado', 'warning'); + return; + } + + const loader = await this.loadingCtrl.create({ + message: 'Gestionando Certificado en SIU Guaraní...
Esto puede demorar unos segundos', + spinner: 'crescent' + }); + await loader.present(); + + try { + const result = await this.siuService.downloadCertificate(creds.dni, creds.pass); + await this.siuService.saveAndOpenFile(result.base64, result.filename); + + this.showToast('¡Certificado generado y abierto!', 'success'); + await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => { }); + } catch (e: any) { + console.error(e); + const alert = await this.alertCtrl.create({ + header: 'Error de Certificado', + message: `No se pudo automatizar la descarga: ${e.message || 'Error desconocido'}. Por favor, intentalo más tarde o desde el SIU web.`, + buttons: ['Entendido'] + }); + await alert.present(); + } finally { + await loader.dismiss(); + } + } + + async showBenefitDetail(type: 'soy' | 'google') { + const modal = await this.modalCtrl.create({ + component: InfoModalComponent, + componentProps: { + selectedSegment: 'benefits' + } + }); + await modal.present(); + } + // --- NUEVAS FUNCIONES CONSOLIDADAS --- async openInfo() { diff --git a/src/app/services/siu-guarani.service.ts b/src/app/services/siu-guarani.service.ts index 5112352..30ddda7 100644 --- a/src/app/services/siu-guarani.service.ts +++ b/src/app/services/siu-guarani.service.ts @@ -1,5 +1,7 @@ import { Injectable } from '@angular/core'; import { CapacitorHttp, HttpResponse } from '@capacitor/core'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { FileOpener } from '@capacitor-community/file-opener'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) @@ -9,7 +11,8 @@ export class SiuService { async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise { try { - await this.loginSiu(dni, pass, carreraId); + await this.authenticateSiu(dni, pass); + await this.selectCareer(carreraId); // 3. OBTENER PLAN DE ESTUDIO const resPlan: HttpResponse = await CapacitorHttp.get({ @@ -36,7 +39,8 @@ export class SiuService { async getInscripcionesActivas(dni: string, pass: string, carreraId: string = '30'): Promise { try { - await this.loginSiu(dni, pass, carreraId); + await this.authenticateSiu(dni, pass); + await this.selectCareer(carreraId); // OBTENER PÁGINA DE CURSADA const resCursada: HttpResponse = await CapacitorHttp.get({ @@ -62,19 +66,189 @@ export class SiuService { } } - private async loginSiu(dni: string, pass: string, carreraId: string): Promise { - // 1. LOGIN POST + private async authenticateSiu(dni: string, pass: string): Promise { + console.log('Autenticando en SIU...'); const loginData = `usuario=${dni}&password=${pass}&login=Ingresar`; await CapacitorHttp.post({ url: `${this.BASE_URL}/acceso?auth=form`, data: loginData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); + } - // 2. CAMBIO DE CARRERA + private async selectCareer(carreraId: string): Promise { + console.log('Seleccionando carrera:', carreraId); await CapacitorHttp.get({ url: `${this.BASE_URL}/acceso/cambiar_carrera?id=${carreraId}&op=inicio_alumno`, }); + // Hit inicio_alumno to ensure context is established + await CapacitorHttp.get({ + url: `${this.BASE_URL}/inicio_alumno`, + }); + } + + private extractTobaContent(html: string, pageletId: string = 'pagelet'): string { + const regexes = [ + /kernel\.renderer\.on_arrival\((\{.*?\})\);/gs, + /on_arrival\((\{.*?\})\);/gs + ]; + + for (const regex of regexes) { + let match; + // Reset regex index for global searches + regex.lastIndex = 0; + while ((match = regex.exec(html)) !== null) { + try { + const jsonData = JSON.parse(match[1]); + if (jsonData.id === pageletId || jsonData.info?.id === pageletId) { + return jsonData.content || ''; + } + } catch (e) { continue; } + } + } + return ''; + } + + async downloadCertificate(dni: string, pass: string): Promise<{ base64: string, filename: string }> { + console.log('Iniciando descarga de certificado para:', dni); + try { + await this.authenticateSiu(dni, pass); + await this.selectCareer('30'); + console.log('Login y selección de carrera (ID 30) listos'); + + // 1. VALIDAR SI EXISTE UNO VIGENTE (< 30 DÍAS) + const resSolicitudes: HttpResponse = await CapacitorHttp.get({ + url: `${this.BASE_URL}/solicitudes`, + }); + console.log('Página de solicitudes obtenida'); + + const contentHtmlSolicitudes = this.extractTobaContent(resSolicitudes.data, 'pagelet'); + if (contentHtmlSolicitudes) { + const existingCert = this.findValidCertificate(contentHtmlSolicitudes); + if (existingCert) { + console.log('Certificado vigente encontrado:', existingCert.filename); + return await this.downloadFile(existingCert.url, existingCert.filename); + } + } + + // 2. GENERAR NUEVA SOLICITUD (HEADLESS) + console.log('No hay certificado vigente o no se pudo leer la tabla. Intentando generar uno nuevo...'); + const resGestion: HttpResponse = await CapacitorHttp.get({ + url: `${this.BASE_URL}/solicitudes/solicitud_gestion`, + }); + + const contentHtmlGestion = this.extractTobaContent(resGestion.data, 'pagelet'); + if (!contentHtmlGestion) { + console.error('HTML completo de gestión no contiene on_arrival de pagelet. Muestra:', resGestion.data.substring(0, 1000)); + throw new Error('No se pudo localizar el generador (content missing)'); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(contentHtmlGestion, 'text/html'); + const iframe = doc.querySelector('iframe#frameSolicitud') as HTMLIFrameElement; + let iframeSrc = iframe?.getAttribute('src'); + + if (!iframeSrc) { + throw new Error('No se pudo localizar el generador (iframe src missing)'); + } + + console.log('Iframe localizado, src:', iframeSrc); + + // Asegurar que la URL del iframe es completa + if (!iframeSrc.startsWith('http')) { + const baseUrlParsed = new URL(this.BASE_URL); + iframeSrc = `${baseUrlParsed.origin}${iframeSrc.startsWith('/') ? '' : '/'}${iframeSrc}`; + } + + // Trigger the print event via URL parameters + const printUrl = iframeSrc.includes('?') + ? `${iframeSrc}&toba_ei_notificar=imprimir` + : `${iframeSrc}?toba_ei_notificar=imprimir`; + + console.log('Solicitando generación de PDF:', printUrl); + return await this.downloadFile(printUrl, `Certificado_Alumno_Regular_${new Date().getTime()}.pdf`); + + } catch (error) { + console.error('Error detallado en SIU Certificate:', error); + throw error; + } + } + + private findValidCertificate(html: string): { url: string, filename: string } | null { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const rows = Array.from(doc.querySelectorAll('tr')); + + for (const row of rows) { + const cells = Array.from(row.querySelectorAll('td')); + if (cells.length >= 3) { + const tipo = cells[0].textContent?.trim().toLowerCase() || ''; + const presentarA = cells[2].textContent?.trim().toLowerCase() || ''; + + // Identificar constancia de alumno regular + if (tipo.includes('alumno regular')) { + const fechaStr = cells[1].textContent?.trim() || ''; + console.log(`Revisando: ${tipo} | Para: ${presentarA} | Fecha: ${fechaStr}`); + + if (this.isWithin30Days(fechaStr)) { + // El usuario indico que si dice "Quien corresponda" o similares (como "Google LLC") sirve. + // Solo descartamos si es para un destinatario muy especifico que no sea el solicitado. + const downloadLink = row.querySelector('a.descargar_cert, a[href*="descargar_comprobante"]') as HTMLAnchorElement; + if (downloadLink) { + return { + url: downloadLink.getAttribute('href') || '', + filename: `Certificado_SIU_${presentarA.replace(/\s+/g, '_')}_${fechaStr.replace(/\//g, '-')}.pdf` + }; + } + } + } + } + } + return null; + } + + private isWithin30Days(fechaStr: string): boolean { + // Formato esperado: DD/MM/YYYY + const parts = fechaStr.split('/'); + if (parts.length !== 3) return false; + const date = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0])); + const diff = new Date().getTime() - date.getTime(); + const days = diff / (1000 * 60 * 60 * 24); + return days < 30; + } + + private async downloadFile(url: string, filename: string): Promise<{ base64: string, filename: string }> { + // Si la URL es relativa, completarla + const fullUrl = url.startsWith('http') ? url : `${this.BASE_URL}/${url.startsWith('/') ? url.substring(1) : url}`; + + const response: HttpResponse = await CapacitorHttp.get({ + url: fullUrl, + responseType: 'blob' + }); + + return { + base64: response.data, // CapacitorHttp con responseType blob devuelve base64 string + filename + }; + } + + async saveAndOpenFile(base64Data: string, filename: string): Promise { + try { + const savedFile = await Filesystem.writeFile({ + path: `Certificados/${filename}`, + data: base64Data, + directory: Directory.Documents, + recursive: true + }); + + await FileOpener.open({ + filePath: savedFile.uri, + contentType: 'application/pdf', + }); + } catch (error) { + console.error('Error saving or opening file:', error); + throw error; + } } private parseInscripcionesActivasHtml(html: string): any[] {