se ha integrado el login con SIU Guarani y se agrego un manual de usuario en info.

This commit is contained in:
2026-03-30 14:03:45 -03:00
parent 678cc9898f
commit 460fbf1634
14 changed files with 815 additions and 332 deletions
+57 -7
View File
@@ -4,6 +4,11 @@
<ion-back-button defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>Configuración de Perfil</ion-title>
<ion-buttons slot="end">
<ion-button (click)="openInfo()">
<ion-icon slot="icon-only" name="information-circle-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
@@ -34,7 +39,8 @@
</ion-card-header>
<ion-card-content *ngIf="!authState?.siuConnected">
<p class="auth-desc">Conectá con el SIU para traer tu historial y que la app auto-favoritee tus materias en curso.</p>
<p class="auth-desc">Conectá con el SIU para traer tu historial y que la app auto-favoritee tus materias en
curso.</p>
<ion-list lines="inset" class="auth-form">
<ion-item>
<ion-icon name="person-outline" slot="start" color="medium"></ion-icon>
@@ -42,7 +48,8 @@
</ion-item>
<ion-item>
<ion-icon name="lock-closed-outline" slot="start" color="medium"></ion-icon>
<ion-input [type]="showSiuPass ? 'text' : 'password'" placeholder="Contraseña de SIU" [(ngModel)]="siuPass"></ion-input>
<ion-input [type]="showSiuPass ? 'text' : 'password'" placeholder="Contraseña de SIU"
[(ngModel)]="siuPass"></ion-input>
<ion-button slot="end" fill="clear" (click)="showSiuPass = !showSiuPass" color="medium">
<ion-icon [name]="showSiuPass ? 'eye-off-outline' : 'eye-outline'"></ion-icon>
</ion-button>
@@ -83,7 +90,8 @@
</ion-card-header>
<ion-card-content *ngIf="!authState?.moodleConnected">
<p class="auth-desc">Vinculá tu cuenta del campus para generar tu credencial digital offline y habilitar alertas de actividades.</p>
<p class="auth-desc">Vinculá tu cuenta del campus para generar tu credencial digital offline y habilitar alertas
de actividades.</p>
<ion-list lines="inset" class="auth-form">
<ion-item>
<ion-icon name="person-outline" slot="start" color="medium"></ion-icon>
@@ -91,7 +99,8 @@
</ion-item>
<ion-item>
<ion-icon name="lock-closed-outline" slot="start" color="medium"></ion-icon>
<ion-input [type]="showMoodlePass ? 'text' : 'password'" placeholder="Contraseña de Campus" [(ngModel)]="moodlePass"></ion-input>
<ion-input [type]="showMoodlePass ? 'text' : 'password'" placeholder="Contraseña de Campus"
[(ngModel)]="moodlePass"></ion-input>
<ion-button slot="end" fill="clear" (click)="showMoodlePass = !showMoodlePass" color="medium">
<ion-icon [name]="showMoodlePass ? 'eye-off-outline' : 'eye-outline'"></ion-icon>
</ion-button>
@@ -119,9 +128,50 @@
</ion-card-content>
</ion-card>
<!-- Sección de Credencial Digital (Solo si está vinculado) -->
<ion-card class="auth-card credencial-card" *ngIf="authState?.moodleConnected" (click)="openCredencial()">
<ion-card-header>
<div class="card-title-row">
<ion-icon name="qr-code" color="primary" class="service-icon"></ion-icon>
<ion-card-title>Mi Credencial Digital</ion-card-title>
<ion-icon name="chevron-forward" color="medium"></ion-icon>
</div>
<ion-card-subtitle>Acceso rápido con QR y modo sin conexión</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<p class="auth-desc">Toca para abrir tu credencial. El brillo se ajustará automáticamente.</p>
</ion-card-content>
</ion-card>
<!-- Sección de Respaldo y Datos -->
<ion-card class="auth-card backup-card">
<ion-card-header>
<div class="card-title-row">
<ion-icon name="cloud-upload" color="tertiary" class="service-icon"></ion-icon>
<ion-card-title>Datos y Respaldo</ion-card-title>
</div>
<ion-card-subtitle>Asegurá tu progreso académico</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<p class="auth-desc">Exportá tus materias favoritas y aprobadas para restaurarlas en otro dispositivo.</p>
<div class="backup-buttons">
<ion-button expand="block" fill="outline" (click)="forzarRespaldo()">
<ion-icon name="save-outline" slot="start"></ion-icon>
Guardar Respaldo
</ion-button>
<ion-button expand="block" fill="outline" color="secondary" (click)="importarRespaldoManual()">
<ion-icon name="cloud-download-outline" slot="start"></ion-icon>
Restaurar Datos
</ion-button>
</div>
</ion-card-content>
</ion-card>
<div class="security-info ion-padding">
<ion-icon name="shield-checkmark" color="medium"></ion-icon>
<p>Tus credenciales se almacenan de forma segura y local en tu dispositivo. No son enviadas a servidores externos ajenos a la universidad.</p>
<ion-icon name="lock-closed" color="medium"></ion-icon>
<p>Tus credenciales y datos se almacenan de forma segura y local en el almacenamiento persistente de tu teléfono.
</p>
</div>
</div>
</ion-content>
</ion-content>
+66 -3
View File
@@ -165,6 +165,71 @@
}
}
.credencial-card {
background: linear-gradient(to right, rgba(var(--ion-color-primary-rgb), 0.05), transparent);
border-left: 6px solid var(--ion-color-primary) !important;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.backup-card {
.backup-buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
ion-button {
--border-radius: 12px;
margin: 0;
font-weight: 600;
}
}
}
.info-section-container {
background: white;
border-radius: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
.section-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--ion-color-dark);
margin: 0 0 15px 0;
}
.info-item {
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;
@@ -180,15 +245,13 @@
text-align: center;
gap: 10px;
opacity: 0.7;
ion-icon {
font-size: 30px;
}
p {
font-size: 0.8rem;
margin: 0;
line-height: 1.4;
max-width: 250px;
max-width: 280px;
}
}
+136 -9
View File
@@ -1,9 +1,14 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ToastController, LoadingController, AlertController, NavController } from '@ionic/angular';
import { ToastController, LoadingController, AlertController, NavController, ActionSheetController, ModalController } from '@ionic/angular';
import { AuthService, AuthState } from '../services/auth.service';
import { SiuService } from '../services/siu-guarani.service';
import { CredencialService } from '../services/credencial.service';
import { LocalStorageService, AppData } from '../services/local-storage.service';
import { EventosService } from '../services/eventos.service';
import { CredencialModalComponent } from '../components/credencial-modal/credencial-modal.component';
import { InfoModalComponent } from '../components/info-modal/info-modal.component';
import { Haptics, ImpactStyle } from '@capacitor/haptics';
import { Preferences } from '@capacitor/preferences';
import { Subscription } from 'rxjs';
@Component({
@@ -32,9 +37,12 @@ export class PerfilPage implements OnInit, OnDestroy {
private siuService: SiuService,
private credencialService: CredencialService,
private localStorageService: LocalStorageService,
private eventosService: EventosService,
private toastCtrl: ToastController,
private loadingCtrl: LoadingController,
private alertCtrl: AlertController,
private actionSheetCtrl: ActionSheetController,
private modalCtrl: ModalController,
private navCtrl: NavController
) { }
@@ -115,16 +123,19 @@ export class PerfilPage implements OnInit, OnDestroy {
await loader.present();
try {
// 1. Validar y generar credencial (descarga PDF)
await this.credencialService.generateCredential(this.moodleDni, this.moodlePass);
// 2. Obtener Perfil (Nombre y Foto)
const profile = await this.credencialService.getUserProfile(this.moodleDni, this.moodlePass);
await this.authService.saveUserProfile(profile.name, profile.photo);
// setupMoodleAccount hace UNA sola autenticación y luego descarga la credencial
// y el perfil usando la misma sesión. Evita el bug de doble login donde la segunda
// llamada a loginMoodle() encuentra al usuario ya autenticado y no encuentra el
// logintoken en la página → "No se pudo obtener el token de seguridad del campus."
const result = await this.credencialService.setupMoodleAccount(
this.moodleDni,
this.moodlePass,
(status) => loader.message = status // actualiza el mensaje del loader en tiempo real
);
// 3. Guardar credenciales de acceso
await this.authService.saveUserProfile(result.profile.name, result.profile.photo);
await this.authService.saveMoodleCredentials(this.moodleDni, this.moodlePass);
this.showToast('¡Perfil sincronizado y Campus conectado!', 'success');
this.moodlePass = '';
} catch (e: any) {
@@ -136,6 +147,7 @@ export class PerfilPage implements OnInit, OnDestroy {
}
}
async logoutMoodle() {
const alert = await this.alertCtrl.create({
header: 'Cerrar Sesión en Campus',
@@ -156,6 +168,121 @@ export class PerfilPage implements OnInit, OnDestroy {
await alert.present();
}
// --- NUEVAS FUNCIONES CONSOLIDADAS ---
async openInfo() {
const modal = await this.modalCtrl.create({
component: InfoModalComponent
});
await modal.present();
}
async openCredencial() {
const modal = await this.modalCtrl.create({
component: CredencialModalComponent
});
await modal.present();
}
async forzarRespaldo() {
// Obtenemos los datos actuales desde el service para asegurar que el backup sea completo
const data = await this.localStorageService.loadData() || this.getDefaultData();
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)) {
// Guardamos los eventos uno por uno (el service se encarga de la persistencia de eventos)
if (imported.eventos) {
for (const ev of imported.eventos) {
await this.eventosService.saveEvento(ev);
}
}
// Guardamos el resto de los datos en el LocalStorageService
// Nota: No sobreescribimos TODO el objeto para no borrar fotos de perfil si las hubiera,
// pero sí actualizamos los datos académicos.
const currentData = await this.localStorageService.loadData() || this.getDefaultData();
const newData = {
...currentData,
favoritos: imported.favoritos || currentData.favoritos,
aprobadas: imported.aprobadas || currentData.aprobadas,
encurso: imported.encurso || currentData.encurso,
optativas: imported.optativas || currentData.optativas,
siuMaterias: imported.siuMaterias || currentData.siuMaterias,
siuLastSync: imported.siuLastSync || currentData.siuLastSync
};
await this.localStorageService.saveData(newData);
// Sincronizar favoritos con Capacitor Preferences para que el resto de la app los vea
if (imported.favoritos) {
await Preferences.set({ key: 'favoritos', value: JSON.stringify(imported.favoritos) });
}
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(() => { });
// Opcional: Podríamos forzar un refresh del authState si fuera necesario
} 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();
}
}
goBack() {
this.navCtrl.back();
}