se ha integrado el login con SIU Guarani y se agrego un manual de usuario en info.
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user