feat: integración de SIU Guaraní, auto-completado y rediseño en grilla de recorrido
This commit is contained in:
@@ -206,9 +206,14 @@
|
||||
<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 style="display: flex; gap: 8px;">
|
||||
<ion-button fill="outline" color="primary" size="small" (click)="forzarRespaldo()" style="margin: 0;" title="Guardar Manualmente" class="header-btn">
|
||||
<ion-icon name="save-outline" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="outline" [color]="siuMaterias && siuMaterias.length > 0 ? 'success' : 'tertiary'" size="small" (click)="openSiuModal()" style="margin: 0;" title="Conectar con SIU Guaraní" class="header-btn">
|
||||
<ion-icon name="school-outline" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</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>
|
||||
@@ -238,29 +243,84 @@
|
||||
<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 }}
|
||||
<span class="m-name">{{ abreviarMateria(row.c1.name) }}</span>
|
||||
<ion-badge *ngIf="getSiuMateriaParaCelda(row.c1.name) as siuMat" [ngClass]="'custom-badge-' + getNotaColor(siuMat.nota)" class="siu-cell-badge">
|
||||
{{ getSiuBadgeText(siuMat) }}
|
||||
</ion-badge>
|
||||
</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 }}
|
||||
<span class="m-name">{{ abreviarMateria(row.c2.name) }}</span>
|
||||
<ion-badge *ngIf="getSiuMateriaParaCelda(row.c2.name) as siuMat" [ngClass]="'custom-badge-' + getNotaColor(siuMat.nota)" class="siu-cell-badge">
|
||||
{{ getSiuBadgeText(siuMat) }}
|
||||
</ion-badge>
|
||||
</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 }}
|
||||
<span class="m-name">{{ abreviarMateria(row.c3.name) }}</span>
|
||||
<ion-badge *ngIf="getSiuMateriaParaCelda(row.c3.name) as siuMat" [ngClass]="'custom-badge-' + getNotaColor(siuMat.nota)" class="siu-cell-badge">
|
||||
{{ getSiuBadgeText(siuMat) }}
|
||||
</ion-badge>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- The SIU Guaraní list at the bottom was removed per user request -->
|
||||
</div>
|
||||
|
||||
<!-- Modal SIU Guaraní (Login) -->
|
||||
<ion-modal [isOpen]="isSiuModalOpen" (didDismiss)="closeSiuModal()" class="siu-login-modal" initialBreakpoint="0.8" [breakpoints]="[0, 0.8, 1]">
|
||||
<ng-template>
|
||||
<ion-header class="ion-no-border">
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Conectar con SIU</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeSiuModal()" [disabled]="siuLoading">Cerrar</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding siu-login-content">
|
||||
<div class="ion-text-center siu-logo-container ion-margin-bottom">
|
||||
<ion-icon name="school" color="primary" style="font-size: 64px;"></ion-icon>
|
||||
<h2 style="margin-top: 10px;">Historia Académica</h2>
|
||||
<p style="color: var(--ion-color-medium);">Ingresá para traer tu recorrido y notas automáticamente a la app.</p>
|
||||
</div>
|
||||
|
||||
<ion-list class="ion-margin-bottom siu-form-list" lines="full">
|
||||
<ion-item>
|
||||
<ion-icon name="person-outline" slot="start" color="medium"></ion-icon>
|
||||
<ion-input type="text" placeholder="DNI (Sin puntos)" [(ngModel)]="siuDni" [disabled]="siuLoading" clearInput required></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-icon name="lock-closed-outline" slot="start" color="medium"></ion-icon>
|
||||
<ion-input type="password" placeholder="Contraseña de SIU" [(ngModel)]="siuPass" [disabled]="siuLoading" clearOnEdit="false" required></ion-input>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<div class="siu-info ion-margin-bottom" style="font-size: 0.85rem; color: var(--ion-color-medium); background: rgba(var(--ion-color-primary-rgb), 0.1); padding: 12px; border-radius: 8px; border-left: 4px solid var(--ion-color-primary);">
|
||||
<p style="margin-top: 0;"><ion-icon name="shield-checkmark-outline"></ion-icon> Privacidad Zero-Trust: Las credenciales no se envían a ningún servidor nuestro. La app se conecta directo con el portal localmente desde tu teléfono.</p>
|
||||
<p style="margin-bottom: 0;"><ion-icon name="alert-circle-outline"></ion-icon> Al conectar, se importará tu última situación curricular.</p>
|
||||
</div>
|
||||
|
||||
<ion-button expand="block" color="primary" class="ion-margin-top" (click)="sincronizarSiu()" [disabled]="siuLoading || !siuDni || !siuPass">
|
||||
<ng-container *ngIf="!siuLoading">
|
||||
<ion-icon name="log-in-outline" slot="start"></ion-icon>
|
||||
Conectar y Sincronizar
|
||||
</ng-container>
|
||||
<ion-spinner *ngIf="siuLoading" name="crescent"></ion-spinner>
|
||||
</ion-button>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
</ion-modal>
|
||||
|
||||
</ion-content>
|
||||
|
||||
<ion-footer class="ion-no-border">
|
||||
|
||||
+719
-517
File diff suppressed because it is too large
Load Diff
+303
-52
@@ -13,6 +13,7 @@ import { LocalStorageService } from '../services/local-storage.service';
|
||||
import { EventosService } from '../services/eventos.service';
|
||||
import { EventoModalComponent } from '../components/evento-modal/evento-modal.component';
|
||||
import { AgendaModalComponent } from '../components/agenda-modal/agenda-modal.component';
|
||||
import { SiuService } from '../services/siu-guarani.service';
|
||||
|
||||
|
||||
export interface ProximaClaseInfo {
|
||||
@@ -66,6 +67,14 @@ export class HomePage implements OnInit {
|
||||
|
||||
carreraSeleccionada: string = 'tecnologias';
|
||||
|
||||
// SIU Guaraní State
|
||||
siuMaterias: any[] = [];
|
||||
siuLastSync: string | null = null;
|
||||
isSiuModalOpen: boolean = false;
|
||||
siuLoading: boolean = false;
|
||||
siuDni: string = '';
|
||||
siuPass: string = '';
|
||||
|
||||
|
||||
// Mapa de días en español al getDay() de JS (0=Dom)
|
||||
private readonly DIAS_DOW: Record<string, number> = {
|
||||
@@ -81,7 +90,8 @@ export class HomePage implements OnInit {
|
||||
private alertCtrl: AlertController,
|
||||
private localStorageService: LocalStorageService,
|
||||
private actionSheetCtrl: ActionSheetController,
|
||||
private eventosService: EventosService
|
||||
private eventosService: EventosService,
|
||||
private siuService: SiuService
|
||||
) { }
|
||||
|
||||
|
||||
@@ -118,6 +128,14 @@ export class HomePage implements OnInit {
|
||||
this.aprobadas = userData.aprobadas || [];
|
||||
this.encurso = userData.encurso || [];
|
||||
this.optativasSeleccionadas = userData.optativas || [];
|
||||
this.siuMaterias = userData.siuMaterias || [];
|
||||
this.siuLastSync = userData.siuLastSync || null;
|
||||
|
||||
// Auto-completar desde SIU si hay datos previamente cacheados
|
||||
if (this.siuMaterias.length > 0) {
|
||||
this.autoCompletarDesdeSiu();
|
||||
}
|
||||
|
||||
// Sincronizar eventos cargados desde el CSV con el servicio de eventos
|
||||
for (const ev of userData.eventos) {
|
||||
await this.eventosService.saveEvento(ev);
|
||||
@@ -219,7 +237,9 @@ export class HomePage implements OnInit {
|
||||
this.aprobadas = imported.aprobadas || [];
|
||||
this.encurso = imported.encurso || [];
|
||||
this.optativasSeleccionadas = imported.optativas || [];
|
||||
|
||||
this.siuMaterias = imported.siuMaterias || [];
|
||||
this.siuLastSync = imported.siuLastSync || null;
|
||||
|
||||
// Sincronizar eventos cargados desde el CSV con el servicio de eventos
|
||||
for (const ev of imported.eventos) {
|
||||
await this.eventosService.saveEvento(ev);
|
||||
@@ -274,12 +294,12 @@ export class HomePage implements OnInit {
|
||||
const ahora = new Date();
|
||||
const hoyIdx = ahora.getDay(); // 0-6
|
||||
const horaActualStr = ahora.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
|
||||
|
||||
let candidata: ProximaClaseInfo | null = null;
|
||||
|
||||
// 1. Buscar si hay alguna clase en curso AHORA
|
||||
const favsHoy = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === hoyIdx);
|
||||
|
||||
|
||||
for (const item of favsHoy) {
|
||||
const parts = (item.horario || '').split('-');
|
||||
if (parts.length >= 2) {
|
||||
@@ -304,7 +324,7 @@ export class HomePage implements OnInit {
|
||||
if (tInicio > horaActualStr) {
|
||||
const targetDate = new Date(ahora);
|
||||
targetDate.setHours(parseInt(mInicio[1], 10), parseInt(mInicio[2], 10), 0, 0);
|
||||
|
||||
|
||||
const diff = targetDate.getTime() - ahora.getTime();
|
||||
if (!candidata || diff < candidata.diffMs) {
|
||||
candidata = { item, diffMs: diff, label: 'Próxima Clase' };
|
||||
@@ -317,14 +337,14 @@ export class HomePage implements OnInit {
|
||||
if (!candidata) {
|
||||
const mananaIdx = (hoyIdx + 1) % 7;
|
||||
const favsManana = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === mananaIdx);
|
||||
|
||||
|
||||
for (const item of favsManana) {
|
||||
const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/);
|
||||
if (mInicio) {
|
||||
const targetDate = new Date(ahora);
|
||||
targetDate.setDate(ahora.getDate() + 1);
|
||||
targetDate.setHours(parseInt(mInicio[1], 10), parseInt(mInicio[2], 10), 0, 0);
|
||||
|
||||
|
||||
const diff = targetDate.getTime() - ahora.getTime();
|
||||
if (!candidata || diff < candidata.diffMs) {
|
||||
candidata = { item, diffMs: diff, label: 'Próxima Clase' };
|
||||
@@ -527,7 +547,7 @@ export class HomePage implements OnInit {
|
||||
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);
|
||||
@@ -632,7 +652,7 @@ export class HomePage implements OnInit {
|
||||
const ahora = new Date().getTime();
|
||||
const futuros = this.getEventosMateria(codigo)
|
||||
.filter(e => new Date(e.fecha).getTime() > ahora);
|
||||
|
||||
|
||||
return futuros.length > 0 ? futuros[0] : null;
|
||||
}
|
||||
|
||||
@@ -667,13 +687,13 @@ export class HomePage implements OnInit {
|
||||
await modal.present();
|
||||
|
||||
await modal.onWillDismiss();
|
||||
|
||||
|
||||
// Al cerrar la agenda, recargamos los eventos locales por si se agregó/editó alguno
|
||||
this.eventos = await this.eventosService.loadEventos();
|
||||
|
||||
|
||||
// Y aseguramos que la próxima clase y los badges se actualicen
|
||||
this.actualizarProximaClase();
|
||||
|
||||
|
||||
// Guardamos progreso por si hubo cambios
|
||||
this.guardarProgreso();
|
||||
}
|
||||
@@ -697,14 +717,14 @@ export class HomePage implements OnInit {
|
||||
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: mat('Taller: Gestión de 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') {
|
||||
@@ -729,13 +749,13 @@ export class HomePage implements OnInit {
|
||||
const modes: ('favoritos' | 'todas' | 'plan')[] = ['favoritos', 'todas', 'plan'];
|
||||
const currentIndex = modes.indexOf(this.viewMode);
|
||||
|
||||
if (event.direction === 2) {
|
||||
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) {
|
||||
} else if (event.direction === 4) {
|
||||
// Swipe Derecha (Tab Anterior)
|
||||
if (currentIndex > 0) {
|
||||
this.viewMode = modes[currentIndex - 1];
|
||||
@@ -821,7 +841,7 @@ export class HomePage implements OnInit {
|
||||
subHeader: cell.isOptativa ? 'Gestión de Materia Optativa' : 'Gestión de Materia',
|
||||
buttons: buttons
|
||||
});
|
||||
|
||||
|
||||
await actionSheet.present();
|
||||
}
|
||||
|
||||
@@ -887,7 +907,9 @@ export class HomePage implements OnInit {
|
||||
eventos: this.eventos,
|
||||
aprobadas: this.aprobadas,
|
||||
optativas: this.optativasSeleccionadas,
|
||||
encurso: this.encurso
|
||||
encurso: this.encurso,
|
||||
siuMaterias: this.siuMaterias,
|
||||
siuLastSync: this.siuLastSync
|
||||
});
|
||||
}
|
||||
|
||||
@@ -898,9 +920,11 @@ export class HomePage implements OnInit {
|
||||
eventos: this.eventos,
|
||||
aprobadas: this.aprobadas,
|
||||
optativas: this.optativasSeleccionadas,
|
||||
encurso: this.encurso
|
||||
encurso: this.encurso,
|
||||
siuMaterias: this.siuMaterias,
|
||||
siuLastSync: this.siuLastSync
|
||||
};
|
||||
|
||||
|
||||
const actionSheet = await this.actionSheetCtrl.create({
|
||||
header: 'Guardar Respaldo',
|
||||
subHeader: '¿A dónde querés exportar tus materias guardadas?',
|
||||
@@ -909,14 +933,14 @@ export class HomePage implements OnInit {
|
||||
text: 'Compartir (Drive, WhatsApp, Mail...)',
|
||||
icon: 'share-social-outline',
|
||||
handler: async () => {
|
||||
await this.ejecutarExportacion(data, false);
|
||||
await this.ejecutarExportacion(data, false);
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Descargar Offline (A la carpeta Descargas)',
|
||||
icon: 'download-outline',
|
||||
handler: async () => {
|
||||
await this.ejecutarExportacion(data, true);
|
||||
await this.ejecutarExportacion(data, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -939,44 +963,271 @@ export class HomePage implements OnInit {
|
||||
icon: 'save'
|
||||
});
|
||||
toast.present();
|
||||
await Haptics.impact({ style: ImpactStyle.Heavy }).catch(() => {});
|
||||
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(() => {});
|
||||
this.favoritos = imported.favoritos;
|
||||
this.aprobadas = imported.aprobadas || [];
|
||||
this.encurso = imported.encurso || [];
|
||||
this.optativasSeleccionadas = imported.optativas || [];
|
||||
this.siuMaterias = imported.siuMaterias || [];
|
||||
this.siuLastSync = imported.siuLastSync || null;
|
||||
|
||||
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();
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'No se procesó ningún archivo válido.',
|
||||
duration: 2500,
|
||||
color: 'warning',
|
||||
icon: 'alert-circle'
|
||||
});
|
||||
toast.present();
|
||||
}
|
||||
}
|
||||
|
||||
// --- SIU Guaraní ---
|
||||
|
||||
openSiuModal() {
|
||||
this.isSiuModalOpen = true;
|
||||
}
|
||||
|
||||
closeSiuModal() {
|
||||
this.isSiuModalOpen = false;
|
||||
this.siuDni = '';
|
||||
this.siuPass = '';
|
||||
}
|
||||
|
||||
async sincronizarSiu() {
|
||||
if (!this.siuDni || !this.siuPass) {
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Por favor, ingresá DNI y Contraseña para conectar',
|
||||
duration: 2500,
|
||||
color: 'warning'
|
||||
});
|
||||
toast.present();
|
||||
return;
|
||||
}
|
||||
|
||||
this.siuLoading = true;
|
||||
try {
|
||||
const materias = await this.siuService.getAcademicHistory(this.siuDni, this.siuPass);
|
||||
this.siuMaterias = materias;
|
||||
this.siuLastSync = new Date().toISOString();
|
||||
|
||||
this.autoCompletarDesdeSiu();
|
||||
this.guardarProgreso();
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Historial del SIU cargado con éxito!',
|
||||
duration: 3000,
|
||||
color: 'success'
|
||||
});
|
||||
toast.present();
|
||||
this.closeSiuModal();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const toast = await this.toastCtrl.create({
|
||||
message: 'Error al conectar con SIU. Comprobá tus datos o intentá más tarde.',
|
||||
duration: 4000,
|
||||
color: 'danger'
|
||||
});
|
||||
toast.present();
|
||||
} finally {
|
||||
this.siuLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getNotaColor(nota: string): string {
|
||||
if (!nota) return 'medium';
|
||||
|
||||
const n = parseInt(nota, 10);
|
||||
if (!isNaN(n)) {
|
||||
if (n >= 4) return 'success';
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
// Fallback if not numeric
|
||||
const lower = nota.toLowerCase();
|
||||
if (lower.includes('aprobado') || lower.includes('promocionado') || lower.includes('equivalencia')) return 'success';
|
||||
if (lower.includes('regular') || lower.includes('en curso')) return 'warning';
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
// --- SIU Guaraní Mapeo y Auto-Sync ---
|
||||
|
||||
getCleanSubjectName(name: string): string {
|
||||
if (!name) return '';
|
||||
return name.split(' (')[0].trim();
|
||||
}
|
||||
|
||||
matcheaSiu(siuName: string, appName: string): boolean {
|
||||
const siuN = this.normalizeText(siuName);
|
||||
const appN = this.normalizeText(this.getCleanSubjectName(appName));
|
||||
if (!siuN || !appN) return false;
|
||||
return siuN === appN || appN.includes(siuN) || siuN.includes(appN);
|
||||
}
|
||||
|
||||
getSiuMateriaParaCelda(materiaNombre: string): any {
|
||||
if (!materiaNombre || !this.siuMaterias) return null;
|
||||
const siuMat = this.siuMaterias.find(m => this.matcheaSiu(m.nombre, materiaNombre));
|
||||
if (siuMat && this.determinarEstadoSiu(siuMat.nota, siuMat.origen) !== null) {
|
||||
return siuMat;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getSiuBadgeText(siuMat: any): string {
|
||||
if (!siuMat) return '';
|
||||
const notaValid = siuMat.nota && siuMat.nota.trim() !== '' && siuMat.nota !== 'SIN NOTA' && siuMat.nota !== '-';
|
||||
let txt = notaValid ? siuMat.nota : '';
|
||||
|
||||
const estado = this.determinarEstadoSiu(siuMat.nota, siuMat.origen);
|
||||
if (estado === 'encurso') {
|
||||
if (txt) {
|
||||
return txt.toLowerCase().includes('curso') ? txt : `${txt} (En Curso)`;
|
||||
}
|
||||
return 'En Curso';
|
||||
}
|
||||
|
||||
let badgeText = txt || 'Aprobado';
|
||||
badgeText = badgeText.replace(/Promocionado/gi, 'Prom.');
|
||||
badgeText = badgeText.replace(/Aprobado/gi, 'Aprob.');
|
||||
return badgeText;
|
||||
}
|
||||
|
||||
abreviarMateria(nombre: string): string {
|
||||
if (!nombre) return '';
|
||||
let n = nombre;
|
||||
// Sustituciones específicas para encajar mejor en los zócalos
|
||||
n = n.replace('Gestión de Sistemas de Información', 'Gest. Sist. de Info.');
|
||||
n = n.replace('Administración de Negocios Digitales', 'Admin. Negocios Dig.');
|
||||
n = n.replace('Seguridad de la Información', 'Seguridad Info.');
|
||||
n = n.replace('Programación de Vanguardia', 'Prog. de Vanguardia');
|
||||
n = n.replace('Taller: Emprendedurismo en Innovación Digital', 'Taller Emprendedurismo');
|
||||
n = n.replace('Taller: Gestión de Proyectos Digitales', 'Taller Proyectos');
|
||||
n = n.replace('Taller: Control de Calidad de Software', 'Taller QA Software');
|
||||
n = n.replace(/Gestión del Talento Humano en la Industria [dD]igital/, 'Gest. Talento Humano');
|
||||
n = n.replace('Gestión de Redes Informáticas', 'Redes Informáticas');
|
||||
n = n.replace('Desarrollo en Dispositivos Móviles', 'Des. Dispositivos Móviles');
|
||||
n = n.replace('Diseño y Producción de Videojuegos', 'Diseño y Prod. Videojuegos');
|
||||
return n;
|
||||
}
|
||||
|
||||
get siuMateriasFiltradas() {
|
||||
if (!this.siuMaterias) return [];
|
||||
const allCareerSubjects: string[] = [];
|
||||
for (const row of this.getRecorridoData()) {
|
||||
allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name);
|
||||
}
|
||||
return this.siuMaterias.filter(mat => {
|
||||
const inCareer = allCareerSubjects.some(appName => this.matcheaSiu(mat.nombre, appName));
|
||||
const hasState = this.determinarEstadoSiu(mat.nota, mat.origen) !== null;
|
||||
return inCareer && hasState;
|
||||
});
|
||||
}
|
||||
|
||||
determinarEstadoSiu(nota: string, origen: string): 'aprobada' | 'encurso' | null {
|
||||
const n = parseInt(nota, 10);
|
||||
const lowerNota = (nota || '').toLowerCase();
|
||||
const lowerOrigen = (origen || '').toLowerCase();
|
||||
|
||||
// Si la nota es numérica y es mayor o igual a 4, está aprobada
|
||||
if (!isNaN(n) && n >= 4) return 'aprobada';
|
||||
if (lowerNota.includes('aprobado') || lowerNota.includes('promocionado') || lowerNota.includes('equivalencia')) return 'aprobada';
|
||||
if (lowerOrigen.includes('aprobado') || lowerOrigen.includes('promocionado')) return 'aprobada';
|
||||
|
||||
// Si no está aprobada, pero cursa regular o está anotado
|
||||
if (lowerOrigen.includes('en curso') || lowerNota.includes('en curso') || lowerOrigen.includes('regular')) return 'encurso';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
autoCompletarDesdeSiu() {
|
||||
let changed = false;
|
||||
|
||||
// Primero, pre-mapear optativas huérfanas
|
||||
const newlyMapped = this.autoAsignarOptativasSiu();
|
||||
if (newlyMapped) changed = true;
|
||||
|
||||
const allCareerSubjects: string[] = [];
|
||||
for (const row of this.getRecorridoData()) {
|
||||
allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name);
|
||||
}
|
||||
|
||||
for (const appName of allCareerSubjects) {
|
||||
if (appName.startsWith('Optativa') && !appName.includes('ASIG')) continue; // Optativa no seleccionada
|
||||
|
||||
const siuMat = this.getSiuMateriaParaCelda(appName);
|
||||
if (siuMat) {
|
||||
const estadoSiu = this.determinarEstadoSiu(siuMat.nota, siuMat.origen);
|
||||
if (estadoSiu === 'aprobada' && !this.aprobadas.includes(appName)) {
|
||||
this.aprobadas = [...this.aprobadas, appName];
|
||||
this.encurso = this.encurso.filter(x => x !== appName);
|
||||
changed = true;
|
||||
} else if (estadoSiu === 'encurso' && !this.aprobadas.includes(appName) && !this.encurso.includes(appName)) {
|
||||
this.encurso = [...this.encurso, appName];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) this.guardarProgreso();
|
||||
}
|
||||
|
||||
autoAsignarOptativasSiu(): boolean {
|
||||
if (!this.siuMaterias) return false;
|
||||
let mappedAlgo = false;
|
||||
|
||||
const allCareerSubjects: string[] = [];
|
||||
for (const row of this.getRecorridoData()) {
|
||||
allCareerSubjects.push(row.c1.name, row.c2.name, row.c3.name);
|
||||
}
|
||||
|
||||
const optativasSiu = this.siuMaterias.filter(mat => {
|
||||
const estado = this.determinarEstadoSiu(mat.nota, mat.origen);
|
||||
if (!estado) return false;
|
||||
return !allCareerSubjects.some(appName => this.matcheaSiu(mat.nombre, appName));
|
||||
});
|
||||
|
||||
if (optativasSiu.length > 0) {
|
||||
let slots = ['tec_opt1', 'tec_opt2', 'tec_opt3'];
|
||||
if (this.carreraSeleccionada === 'datos') slots = ['dat_opt1', 'dat_opt2', 'dat_opt3'];
|
||||
else if (this.carreraSeleccionada === 'videojuegos') slots = ['vid_opt1', 'vid_opt2', 'vid_opt3'];
|
||||
|
||||
for (const siuOpt of optativasSiu) {
|
||||
const cleanName = this.getCleanSubjectName(siuOpt.nombre);
|
||||
const alreadySelected = this.optativasSeleccionadas.find(o => this.matcheaSiu(siuOpt.nombre, o.materia));
|
||||
if (!alreadySelected) {
|
||||
const freeSlot = slots.find(s => !this.optativasSeleccionadas.find(o => o.slot === s));
|
||||
if (freeSlot) {
|
||||
this.optativasSeleccionadas.push({ slot: freeSlot, materia: cleanName });
|
||||
// actualizamos el map de todo para que no intente la próxima vuelta
|
||||
allCareerSubjects.push(cleanName);
|
||||
mappedAlgo = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappedAlgo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface AppData {
|
||||
aprobadas: string[];
|
||||
optativas: {slot: string, materia: string}[];
|
||||
encurso: string[];
|
||||
siuMaterias?: {nombre: string, nota: string, origen: string}[];
|
||||
siuLastSync?: string | null;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@@ -55,7 +57,9 @@ export class LocalStorageService {
|
||||
eventos: parsed.eventos || [],
|
||||
aprobadas: parsed.aprobadas || [],
|
||||
optativas: parsed.optativas || [],
|
||||
encurso: parsed.encurso || []
|
||||
encurso: parsed.encurso || [],
|
||||
siuMaterias: parsed.siuMaterias || [],
|
||||
siuLastSync: parsed.siuLastSync || null
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -130,7 +134,9 @@ export class LocalStorageService {
|
||||
eventos: parsed.eventos || [],
|
||||
aprobadas: parsed.aprobadas || [],
|
||||
optativas: parsed.optativas || [],
|
||||
encurso: parsed.encurso || []
|
||||
encurso: parsed.encurso || [],
|
||||
siuMaterias: parsed.siuMaterias || [],
|
||||
siuLastSync: parsed.siuLastSync || null
|
||||
};
|
||||
await this.saveData(importedData);
|
||||
return importedData;
|
||||
@@ -208,7 +214,7 @@ export class LocalStorageService {
|
||||
}
|
||||
|
||||
const importedData: AppData = {
|
||||
favoritos: favs, eventos: evs, aprobadas: aprobadas, optativas: optativasMap, encurso: encurso
|
||||
favoritos: favs, eventos: evs, aprobadas: aprobadas, optativas: optativasMap, encurso: encurso, siuMaterias: [], siuLastSync: null
|
||||
};
|
||||
|
||||
if (saveAfter) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CapacitorHttp, HttpResponse } from '@capacitor/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SiuService {
|
||||
private readonly BASE_URL = 'https://siuuniciu.eastus2.cloudapp.azure.com/caba';
|
||||
|
||||
async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise<any[]> {
|
||||
try {
|
||||
// 1. LOGIN POST
|
||||
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 (Simulando el GET que capturamos)
|
||||
// Esto es lo que destrabó el cuello de botella
|
||||
await CapacitorHttp.get({
|
||||
url: `${this.BASE_URL}/acceso/cambiar_carrera?id=${carreraId}&op=inicio_alumno`,
|
||||
});
|
||||
|
||||
// 3. OBTENER PLAN DE ESTUDIO
|
||||
const resPlan: HttpResponse = await CapacitorHttp.get({
|
||||
url: `${this.BASE_URL}/plan_estudio`,
|
||||
});
|
||||
|
||||
// 4. PARSEO DEL JSON EMBEBIDO
|
||||
// Buscamos el bloque on_arrival que contiene "info_plan"
|
||||
const rawHtml = resPlan.data;
|
||||
const regex = /on_arrival\((\{.*?"id":"info_plan".*?\})\);/s;
|
||||
const match = rawHtml.match(regex);
|
||||
|
||||
if (match && match[1]) {
|
||||
const jsonData = JSON.parse(match[1]);
|
||||
const contentHtml = jsonData.content;
|
||||
return this.parseHtmlSubjects(contentHtml);
|
||||
}
|
||||
|
||||
throw new Error('No se encontró el bloque de materias');
|
||||
} catch (error) {
|
||||
console.error('Error SIU:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private parseHtmlSubjects(html: string): any[] {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const rows = Array.from(doc.querySelectorAll('tr'));
|
||||
|
||||
return rows.map(row => {
|
||||
const cols = row.querySelectorAll('td');
|
||||
if (cols.length >= 6) {
|
||||
const nombreRaw = cols[0].textContent?.trim() || '';
|
||||
return {
|
||||
nombre: nombreRaw.split(' (')[0],
|
||||
nota: cols[4].textContent?.trim(),
|
||||
origen: cols[5].textContent?.trim()
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(item => item && item.nombre);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user