v1.2.3: Fix SIU SSL, matching de comisiones/equipos, credencial QR ampliada y actualizacion de beneficios

This commit is contained in:
2026-08-28 14:12:44 -03:00
parent b2d31db5a8
commit 29284ae131
13 changed files with 741 additions and 183 deletions
@@ -7,7 +7,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-content class="credential-content">
<!-- Loading State -->
<div class="state-container" *ngIf="state === 'loading'">
@@ -54,7 +54,9 @@
<!-- Success Credential State -->
<div class="state-container credential-container" *ngIf="state === 'success'">
<img [src]="'data:image/png;base64,' + credentialImage" class="credential-img" alt="Credencial UNICABA" />
<div class="credential-wrapper">
<img [src]="'data:image/png;base64,' + credentialImage" class="credential-img" alt="Credencial UNICABA" />
</div>
<div class="credential-actions">
<ion-button expand="block" color="primary" fill="outline" (click)="generate(true)">
@@ -4,7 +4,7 @@
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
padding: 20px 16px;
}
.loading-text {
@@ -31,7 +31,7 @@
.login-card {
background: var(--ion-card-background, #ffffff);
color: var(--ion-text-color, #000000);
padding: 30px;
padding: 30px 20px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
width: 100%;
@@ -57,23 +57,37 @@
.credential-container {
justify-content: flex-start;
padding-top: 40px;
min-height: auto;
padding: 8px 8px 24px 8px;
width: 100%;
.credential-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.credential-img {
width: 100%;
max-width: 400px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 450px;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
border: 1px solid #efefef;
margin-bottom: 30px;
margin-bottom: 18px;
display: block;
object-fit: contain;
}
.credential-actions {
width: 100%;
max-width: 400px;
max-width: 450px;
padding: 0 4px;
ion-button {
margin-top: 10px;
margin-top: 6px;
font-weight: 600;
letter-spacing: 0.5px;
}
}
}
@@ -94,7 +94,7 @@
<p>No usamos Google Analytics ni SDKs de seguimiento. No hay publicidad. Cero telemetría invasiva.</p>
<div class="footer-info ion-text-center">
<p><small>Aulas UNICABA v1.2.2 • Independiente y Offline-First</small></p>
<p><small>Aulas UNICABA v1.2.3 • Independiente y Offline-First</small></p>
</div>
</div>
</div>
@@ -103,51 +103,223 @@
<div *ngIf="selectedSegment === 'benefits'" class="fade-in">
<div class="logo-container ion-text-center">
<ion-icon name="gift-outline" size="large" color="tertiary"></ion-icon>
<h2>Guía de Beneficios</h2>
<p class="subtitle">Aprovechá tu formación en UNICABA</p>
<h2>Comunidad de Beneficios</h2>
<p class="subtitle">Descuentos exclusivos para la Comunidad UdelaCiudad</p>
</div>
<div class="info-body">
<section class="benefit-section">
<div class="benefit-header">
<ion-icon name="bus" color="primary"></ion-icon>
<h3>1. SOY Estudiante (Transporte)</h3>
</div>
<p>Obtené descuentos en pasajes de micro a todo el país a través del portal de <b>Central de Pasajes</b>.</p>
<div class="steps-box">
<h4>¿Cómo activarlo?</h4>
<ol>
<li>Entra a <b>Perfil</b> y generá tu <b>Certificado de Alumno Regular</b>.</li>
<li>Ingresá a <a href="https://soyestudiante.com.ar/" target="_blank">soyestudiante.com.ar</a>.</li>
<li>Subí el PDF que guardaste en tu teléfono.</li>
<li>¡Listo! Tu DNI quedará habilitado para descuentos automáticos.</li>
</ol>
</div>
</section>
<div class="benefits-container">
<section class="benefit-section">
<div class="benefit-header">
<ion-icon name="logo-google" color="danger"></ion-icon>
<h3>2. Google Students (IA y Nube)</h3>
<!-- ON FIT (DESTACADO) -->
<div class="benefit-card featured">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon gym">
<ion-icon name="barbell-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>ON FIT</h3>
<p class="category">Gimnasio & Bienestar</p>
</div>
</div>
<ion-badge color="success" class="discount-badge">25% OFF</ion-badge>
</div>
<p>Cargando la <b>Constancia de Alumno Regular</b> con tu cuenta personal te da acceso a herramientas
profesionales.
<p class="benefit-desc">
25% de descuento sobre el valor de la membresía mensual con acceso libre a sus 20 sedes en todo el AMBA.
</p>
<div class="steps-box">
<h4>Beneficios incluidos:</h4>
<div class="inclusion-box">
<h4>¿Qué incluye la membresía?</h4>
<ul>
<li><strong>Google Gemini PRO:</strong> Bonificación por un año de IA avanzada.</li>
<li><strong>Google Drive:</strong> Almacenamiento ampliado (hasta 1TB según vigencia).</li>
<li><strong>Meet Premium:</strong> Reuniones sin límite de tiempo y grabación directa.</li>
<li>Instalaciones con máquinas Technogym de última generación.</li>
<li>Plan de entrenamiento personalizado, actualizado mensualmente.</li>
<li>Clases grupales: funcional, zumba, stretching y más.</li>
<li>Lockers gratuitos en todas las sedes.</li>
</ul>
<p><small>* Para validar, usá tu mail institucional y subí el certificado si se solicita comprobante de
cursada.</small></p>
</div>
</section>
<div class="footer-info ion-text-center">
<p><small>Los beneficios son gestionados por terceros y sujetos a sus términos y condiciones.</small></p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="information-circle-outline"></ion-icon>
<span>Presentá tu Credencial Digital de la app para acreditar pertenencia a la Universidad.</span>
</div>
</div>
<ion-button expand="block" color="success" class="action-btn"
href="https://wa.me/5491158500830?text=Hola!%20Soy%20de%20la%20Universidad%20de%20la%20Ciudad%20y%20deseo%20acceder%20al%20beneficio%20ON%20FIT"
target="_blank">
<ion-icon name="logo-whatsapp" slot="start"></ion-icon>
Consultar por WhatsApp (+54 9 11 5850-0830)
</ion-button>
</div>
<!-- BERESHIT -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon cafe">
<ion-icon name="cafe-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>BERESHIT</h3>
<p class="category">Cafetería</p>
</div>
</div>
<ion-badge color="warning" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Tte. Gral. Perón 893, CABA <em>(a metros de la sede)</em></span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 8 a 20 h • Sábados de 8 a 16 h</span>
</div>
</div>
</div>
<!-- CAFÉ & CHOCOLATE -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon cafe">
<ion-icon name="restaurant-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Café & Chocolate</h3>
<p class="category">Cafetería & Delicatessen</p>
</div>
</div>
<ion-badge color="warning" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Bartolomé Mitre 807, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 8 a 19 h • Sábados de 8 a 14 h</span>
</div>
</div>
</div>
<!-- AL BUEN TALLARÍN DE PABLO -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon pasta">
<ion-icon name="pizza-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Al Buen Tallarín de Pablo</h3>
<p class="category">Fábrica de Pastas</p>
</div>
</div>
<ion-badge color="danger" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Tucumán 1596, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Mar a Vie de 8 a 19 h • Sáb y Dom de 9:30 a 14:30 h</span>
</div>
</div>
</div>
<!-- ATLAS COMERCIAL -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon libreria">
<ion-icon name="book-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Atlas Comercial</h3>
<p class="category">Librería & Papelería</p>
</div>
</div>
<ion-badge color="primary" class="discount-badge">10% OFF</ion-badge>
</div>
<p class="benefit-desc">10% de descuento en productos (excluye promociones vigentes).</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Bartolomé Mitre 839, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="time-outline"></ion-icon>
<span>Lun a Vie de 9 a 18 h</span>
</div>
</div>
</div>
<!-- FLUIRE STUDIO -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon pilates">
<ion-icon name="body-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>Fluire Studio</h3>
<p class="category">Pilates & Salud</p>
</div>
</div>
<ion-badge color="tertiary" class="discount-badge">Hasta 20% OFF</ion-badge>
</div>
<p class="benefit-desc">
10% de descuento abonando 1 mes de servicio y 20% de descuento abonando 4 meses de servicio.
</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="location-outline"></ion-icon>
<span>Esmeralda 135, piso 4 B, CABA</span>
</div>
<div class="meta-row">
<ion-icon name="mail-outline"></ion-icon>
<span>Contacto: fluire.studio.ba@gmail.com</span>
</div>
</div>
<ion-button expand="block" fill="outline" color="tertiary" class="action-btn"
href="mailto:fluire.studio.ba@gmail.com?subject=Consulta%20Beneficio%20Universidad%20de%20la%20Ciudad">
<ion-icon name="mail-outline" slot="start"></ion-icon>
Contactar por Email
</ion-button>
</div>
<!-- SOY ESTUDIANTE -->
<div class="benefit-card">
<div class="card-top">
<div class="brand-info">
<div class="brand-icon travel">
<ion-icon name="bus-outline"></ion-icon>
</div>
<div class="title-wrap">
<h3>SOY Estudiante</h3>
<p class="category">Transporte de Larga Distancia</p>
</div>
</div>
<ion-badge color="secondary" class="discount-badge">Descuento Nacional</ion-badge>
</div>
<p class="benefit-desc">
Obtené descuentos en pasajes de micro a todo el país a través del portal de Central de Pasajes.
</p>
<div class="meta-grid">
<div class="meta-row">
<ion-icon name="checkmark-circle-outline"></ion-icon>
<span>Entrá a <b>Perfil</b>, generá tu Certificado de Alumno Regular y subilo a <a href="https://soyestudiante.com.ar/" target="_blank">soyestudiante.com.ar</a>.</span>
</div>
</div>
</div>
</div>
<div class="footer-info ion-text-center">
<p><small>Presentá tu credencial digital de Aulas UNICABA en los comercios adheridos para acceder a los descuentos.</small></p>
</div>
</div>
@@ -76,6 +76,153 @@ ion-segment {
}
}
/* --- ESTILOS DE BENEFICIOS --- */
.benefits-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 25px;
}
.benefit-card {
background: white;
border-radius: 18px;
padding: 18px;
box-shadow: 0 4px 15px rgba(0,0,0,0.06);
border: 1px solid rgba(0,0,0,0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&.featured {
border: 1.5px solid var(--ion-color-primary);
background: linear-gradient(180deg, #ffffff 0%, rgba(var(--ion-color-primary-rgb), 0.02) 100%);
box-shadow: 0 6px 20px rgba(var(--ion-color-primary-rgb), 0.12);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.brand-info {
display: flex;
align-items: center;
gap: 12px;
.brand-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(var(--ion-color-primary-rgb), 0.1);
color: var(--ion-color-primary);
font-size: 24px;
flex-shrink: 0;
&.cafe { background: #fff3e0; color: #e65100; }
&.gym { background: #e8f5e9; color: #2e7d32; }
&.pilates { background: #ede7f6; color: #512da8; }
&.pasta { background: #fbe9e7; color: #d84315; }
&.libreria { background: #e1f5fe; color: #0288d1; }
&.travel { background: #e3f2fd; color: #1565c0; }
}
.title-wrap {
h3 {
margin: 0;
font-size: 1.05rem;
font-weight: 800;
color: var(--ion-color-dark);
}
.category {
margin: 2px 0 0;
font-size: 0.8rem;
color: var(--ion-color-medium);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
.discount-badge {
font-size: 0.78rem;
font-weight: 800;
padding: 5px 10px;
border-radius: 20px;
letter-spacing: 0.3px;
}
}
.benefit-desc {
font-size: 0.92rem;
color: var(--ion-color-dark-shade);
margin: 0 0 12px;
line-height: 1.45;
}
.meta-grid {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
margin-bottom: 12px;
.meta-row {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 0.84rem;
color: var(--ion-color-medium-shade);
line-height: 1.4;
ion-icon {
font-size: 16px;
margin-top: 2px;
color: var(--ion-color-primary);
flex-shrink: 0;
}
}
}
.inclusion-box {
background: #f8fafc;
border-radius: 12px;
padding: 12px 14px;
margin: 10px 0;
border-left: 3px solid var(--ion-color-success);
h4 {
margin: 0 0 6px;
font-size: 0.85rem;
font-weight: 700;
color: var(--ion-color-dark);
}
ul {
margin: 0;
padding-left: 18px;
li {
font-size: 0.82rem;
margin-bottom: 4px;
color: #475569;
&:last-child { margin-bottom: 0; }
}
}
}
.action-btn {
--border-radius: 12px;
font-weight: 700;
font-size: 0.88rem;
margin-top: 6px;
letter-spacing: 0.3px;
}
}
.footer-info {
margin-top: 30px;
opacity: 0.6;
+2 -2
View File
@@ -103,8 +103,8 @@
<!-- Icono condicional: Hamburguesa en favoritos, Estrella en cartelera -->
<ion-icon class="menu-icon"
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item.codigo) ? 'star' : 'star-outline')"
[color]="(viewMode === 'todas' && isFavorito(item.codigo)) ? 'warning' : 'medium'"
[name]="viewMode === 'favoritos' ? 'menu-outline' : (isFavorito(item) ? 'star' : 'star-outline')"
[color]="(viewMode === 'todas' && isFavorito(item)) ? 'warning' : 'medium'"
(click)="openMateriaActions(item)"></ion-icon>
</div>
</div>
+137 -24
View File
@@ -108,7 +108,7 @@ export class HomePage implements OnInit {
this.highlightedCodigo = params['highlight'] || null;
if (this.highlightedCodigo) {
// Aseguramos que el highlight sea visible (si no es favorito, ir a 'todas')
if (this.viewMode === 'favoritos' && !this.favoritos.includes(this.highlightedCodigo)) {
if (this.viewMode === 'favoritos' && !this.isFavorito(this.highlightedCodigo)) {
this.viewMode = 'todas';
}
}
@@ -223,7 +223,7 @@ export class HomePage implements OnInit {
this.errorMsg = '';
// Programar recordatorios de clase 30 min antes
const favItems = this.horarios.filter(h => this.favoritos.includes(h.codigo));
const favItems = this.horarios.filter(h => this.isFavorito(h));
this.dataService.scheduleClassReminders(favItems);
// Auto-reparación/Actualización del Respaldo
@@ -262,7 +262,7 @@ export class HomePage implements OnInit {
let baseList = this.horarios;
if (this.viewMode === 'favoritos') {
baseList = baseList.filter(h => this.favoritos.includes(h.codigo));
baseList = baseList.filter(h => this.isFavorito(h));
this.actualizarProximaClase();
} else {
this.proximaClase = null;
@@ -293,7 +293,7 @@ export class HomePage implements OnInit {
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);
const favsHoy = this.horarios.filter(h => this.isFavorito(h) && this.DIAS_DOW[h.dia || ''] === hoyIdx);
for (const item of favsHoy) {
const parts = (item.horario || '').split('-');
@@ -331,7 +331,7 @@ export class HomePage implements OnInit {
// Si no encontramos hoy, buscamos mañana
if (!candidata) {
const mananaIdx = (hoyIdx + 1) % 7;
const favsManana = this.horarios.filter(h => this.isFavorito(h.codigo) && this.DIAS_DOW[h.dia || ''] === mananaIdx);
const favsManana = this.horarios.filter(h => this.isFavorito(h) && this.DIAS_DOW[h.dia || ''] === mananaIdx);
for (const item of favsManana) {
const mInicio = (item.horario || '').match(/(\d{1,2}):(\d{2})/);
@@ -456,12 +456,36 @@ export class HomePage implements OnInit {
this.filterHorarios();
}
isFavorito(codigo: string): boolean {
return this.favoritos.includes(codigo);
getItemKey(item: ClaseHorario): string {
if (!item) return '';
return `${item.codigo}::${item.asignatura.trim()}`;
}
isFavorito(item: ClaseHorario | string): boolean {
if (!item) return false;
if (typeof item === 'string') {
return this.favoritos.some(fav => fav === item || fav.startsWith(`${item}::`));
}
const itemKey = this.getItemKey(item);
// 1. Coincidencia directa por clave específica (ej: "ASIG00127::Programación de Vanguardia - Equipo 1")
if (this.favoritos.includes(itemKey)) return true;
// 2. Coincidencia por código base (ej: "ASIG00125")
if (this.favoritos.includes(item.codigo)) {
// Si en favoritos existe una clave más específica para este código (ej: "ASIG00127::..."),
// el código plano NO debe marcar a las demás variantes
const hasSpecificForCode = this.favoritos.some(fav => fav.startsWith(`${item.codigo}::`));
return !hasSpecificForCode;
}
return false;
}
async toggleFavorito(item: ClaseHorario) {
const isFav = this.isFavorito(item.codigo);
const isFav = this.isFavorito(item);
const itemKey = this.getItemKey(item);
const alert = await this.alertCtrl.create({
header: isFav ? 'Quitar de Favoritos' : 'Agregar a Favoritos',
@@ -477,9 +501,22 @@ export class HomePage implements OnInit {
text: 'Aceptar',
handler: async () => {
if (isFav) {
this.favoritos = this.favoritos.filter(id => id !== item.codigo);
// Remover tanto la clave específica como el código genérico si existía
this.favoritos = this.favoritos.filter(id => id !== itemKey && id !== item.codigo);
} else {
this.favoritos.push(item.codigo);
// Verificar si hay múltiples filas con el mismo código en la cartelera (ej. Equipo 1 y Equipo 2)
const sameCodeCount = this.horarios.filter(h => h.codigo === item.codigo).length;
if (sameCodeCount > 1) {
// Hay múltiples equipos/comisiones: guardar clave específica para esta materia y equipo
if (!this.favoritos.includes(itemKey)) {
this.favoritos.push(itemKey);
}
} else {
// Código único: guardar código base
if (!this.favoritos.includes(item.codigo) && !this.favoritos.includes(itemKey)) {
this.favoritos.push(item.codigo);
}
}
}
await Preferences.set({ key: 'favoritos', value: JSON.stringify(this.favoritos) });
this.guardarProgreso();
@@ -572,7 +609,7 @@ export class HomePage implements OnInit {
async openMateriaActions(item: ClaseHorario) {
// Si estamos en cartelera general y NO es favorito, lo agregamos directamente
if (this.viewMode === 'todas' && !this.isFavorito(item.codigo)) {
if (this.viewMode === 'todas' && !this.isFavorito(item)) {
this.toggleFavorito(item);
return;
}
@@ -1073,32 +1110,56 @@ export class HomePage implements OnInit {
// --- FAVORITOS: Sincronización Inteligente ---
if (this.horarios && this.horarios.length > 0) {
// Prioridad 1: Por Código Exacto (Desde la página de Inscripción / Cursada)
// Prioridad 1: Por Código e Identificación Precisa de Equipo/Comisión (Desde Inscripciones Activas de SIU)
if (inscripcionesSiu.length > 0) {
for (const insc of inscripcionesSiu) {
// Buscamos en nuestra cartelera si existe este código ASIG
const matchHorarios = this.horarios.filter(h => h.codigo === insc.codigo);
for (const h of matchHorarios) {
if (!this.favoritos.includes(h.codigo)) {
this.favoritos = [...this.favoritos, h.codigo];
changed = true;
const matchedHorario = this.encontrarHorarioParaInscripcionSiu(insc, this.horarios);
if (matchedHorario) {
const itemKey = this.getItemKey(matchedHorario);
const sameCodeItems = this.horarios.filter(h => h.codigo === matchedHorario.codigo);
if (sameCodeItems.length > 1) {
// Si hay múltiples equipos/comisiones (ej. Equipo 1 y Equipo 2):
// Removemos el código genérico u otros equipos de este mismo código para dejar solo el correcto
const oldFavsCount = this.favoritos.length;
this.favoritos = this.favoritos.filter(f => f !== matchedHorario.codigo && !sameCodeItems.some(h => f === this.getItemKey(h) && f !== itemKey));
if (!this.favoritos.includes(itemKey)) {
this.favoritos.push(itemKey);
changed = true;
} else if (this.favoritos.length !== oldFavsCount) {
changed = true;
}
} else {
// Código único
if (!this.isFavorito(matchedHorario)) {
this.favoritos.push(matchedHorario.codigo);
changed = true;
}
}
}
}
}
// Prioridad 2: Por Nombre (Caso de fallback o si no hay códigos en la cartelera todavía)
// Prioridad 2: Por Nombre (Fallback si la materia no vino en inscripciones activas)
if (this.siuMaterias && this.siuMaterias.length > 0) {
const enCursoEnSiu = this.siuMaterias.filter(m => this.determinarEstadoSiu(m.nota, m.origen) === 'encurso');
for (const siuMat of enCursoEnSiu) {
// Si ya la tenemos por código, omitimos el sync por nombre para evitar duplicados erroneos
const yaEnFavs = this.horarios.some(h => this.favoritos.includes(h.codigo) && this.matcheaSiu(siuMat.nombre, h.asignatura));
const yaEnFavs = this.horarios.some(h => this.isFavorito(h) && this.matcheaSiu(siuMat.nombre, h.asignatura));
if (yaEnFavs) continue;
const matchHorarios = this.horarios.filter(h => this.matcheaSiu(siuMat.nombre, h.asignatura));
for (const h of matchHorarios) {
if (!this.favoritos.includes(h.codigo)) {
this.favoritos = [...this.favoritos, h.codigo];
if (matchHorarios.length === 1) {
const h = matchHorarios[0];
if (!this.isFavorito(h)) {
this.favoritos.push(h.codigo);
changed = true;
}
} else if (matchHorarios.length > 1) {
const h = matchHorarios[0];
const itemKey = this.getItemKey(h);
if (!this.isFavorito(h)) {
this.favoritos.push(itemKey);
changed = true;
}
}
@@ -1115,6 +1176,58 @@ export class HomePage implements OnInit {
if (changed) this.guardarProgreso();
}
/**
* Resuelve la discrepancia entre el código con sufijo de SIU (ej: ASIG00127-E1-TD)
* y los registros de la Cartelera (código ASIG00127 con nombre 'Programación de Vanguardia - Equipo 1')
*/
encontrarHorarioParaInscripcionSiu(insc: { nombre: string, codigo: string }, horarios: ClaseHorario[]): ClaseHorario | null {
if (!insc || !horarios || horarios.length === 0) return null;
// 1. Extraer código base ASIG (ej: de "ASIG00127-E1-TD" extrae "ASIG00127")
const asigMatch = insc.codigo.match(/ASIG\d+/i);
const asigBase = asigMatch ? asigMatch[0].toUpperCase() : '';
// 2. Extraer Equipo / Comisión del código SIU (ej: de "ASIG00127-E1-TD" extrae "1")
const siuTeamMatch = insc.codigo.match(/[-_](?:E|EQ|EQUIPO|C|COM|COMISION)\s*([0-9A-Za-z]+)/i);
const siuTeam = siuTeamMatch ? siuTeamMatch[1].toUpperCase() : null;
// 3. Filtrar candidatos por código ASIG base o por coincidencia de nombre
let candidatos = horarios.filter(h => {
const codeMatch = asigBase && h.codigo.toUpperCase() === asigBase;
const nameMatch = this.matcheaSiu(insc.nombre, h.asignatura);
return codeMatch || nameMatch;
});
if (candidatos.length === 0) return null;
if (candidatos.length === 1) return candidatos[0];
// 4. Si hay múltiples candidatos (ej. Equipo 1 y Equipo 2):
if (siuTeam) {
// Buscar el candidato cuyo nombre de asignatura coincida con el Equipo/Comisión (ej: "Equipo 1", "Eq 1", "Comisión 1")
const candidatoEquipo = candidatos.find(h => {
const sheetTeamMatch = h.asignatura.match(/(?:Equipo|Eq\.?|Comisi[oó]n|Com\.?)\s*([0-9A-Za-z]+)/i);
const sheetTeam = sheetTeamMatch ? sheetTeamMatch[1].toUpperCase() : null;
return sheetTeam === siuTeam;
});
if (candidatoEquipo) return candidatoEquipo;
}
// 5. Fallback por carrera si está disponible (ej: TD -> TEC. DIG.)
if (insc.codigo.toUpperCase().includes('-TD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('TEC') || h.carrera.toUpperCase().includes('DIG'));
if (candidatoCarrera) return candidatoCarrera;
} else if (insc.codigo.toUpperCase().includes('-CD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('DATOS'));
if (candidatoCarrera) return candidatoCarrera;
} else if (insc.codigo.toUpperCase().includes('-VJ') || insc.codigo.toUpperCase().includes('-VD')) {
const candidatoCarrera = candidatos.find(h => h.carrera.toUpperCase().includes('VIDEO'));
if (candidatoCarrera) return candidatoCarrera;
}
return candidatos[0];
}
autoAsignarOptativasSiu(): boolean {
if (!this.siuMaterias) return false;
let mappedAlgo = false;
+1 -1
View File
@@ -316,7 +316,7 @@ export class CredencialService {
const page = await pdf.getPage(1);
const scale = 2.0;
const scale = 3.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
+12 -1
View File
@@ -185,11 +185,22 @@ export class DataService {
return (hash % 2_000_000) + 100_000;
}
private isItemInFavorites(item: ClaseHorario, favoritos: string[]): boolean {
const itemKey = `${item.codigo}::${item.asignatura.trim()}`;
if (favoritos.includes(itemKey)) return true;
if (favoritos.includes(item.codigo)) {
const hasSpecific = favoritos.some(f => f.startsWith(`${item.codigo}::`));
return !hasSpecific;
}
return false;
}
private async checkRoomChanges(oldData: ClaseHorario[], newData: ClaseHorario[], favoritos: string[]) {
for (const item of newData) {
if (favoritos.includes(item.codigo)) {
if (this.isItemInFavorites(item, favoritos)) {
const oldItem = oldData.find(o =>
o.codigo === item.codigo &&
o.asignatura === item.asignatura &&
o.dia === item.dia &&
o.horario === item.horario
);
+53 -12
View File
@@ -7,7 +7,7 @@ import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class SiuService {
constructor(private authService: AuthService) {}
private readonly BASE_URL = 'https://siuuniciu.eastus2.cloudapp.azure.com/caba';
private readonly BASE_URL = 'https://autogestion.udelaciudad.edu.ar/caba';
async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise<any[]> {
try {
@@ -49,7 +49,27 @@ export class SiuService {
const rawHtml = resCursada.data;
// Buscamos el bloque on_arrival que contiene "info_materia" (donde están las inscripciones)
// 1. Extraer directamente de "notificaciones" donde SIU provee el array JSON "asignaciones"
const regexNotif = /on_arrival\((\{.*?"id":"notificaciones".*?\})\);/s;
const matchNotif = rawHtml.match(regexNotif);
if (matchNotif && matchNotif[1]) {
try {
const jsonNotif = JSON.parse(matchNotif[1]);
const asignaciones = jsonNotif.info?.asignaciones;
if (Array.isArray(asignaciones) && asignaciones.length > 0) {
return asignaciones.map((asig: any) => ({
nombre: asig.NOMBRE_MATERIA || asig.MATERIA,
codigo: asig.NOMBRE || asig.MATERIA, // ej: "ASIG00127-E1-TD"
comision: asig.COMISION,
dia: asig.DIA_SEMANA
}));
}
} catch (e) {
console.warn('No se pudieron leer asignaciones estructuradas:', e);
}
}
// 2. Fallback: extraer del bloque HTML "info_materia" (tabla .js-mis-inscripciones)
const regex = /on_arrival\((\{.*?"id":"info_materia".*?\})\);/s;
const match = rawHtml.match(regex);
@@ -59,7 +79,7 @@ export class SiuService {
return this.parseInscripcionesActivasHtml(contentHtml);
}
return []; // Quizás no tiene inscripciones activas
return [];
} catch (error) {
console.error('Error SIU Active Inscriptions:', error);
return [];
@@ -254,20 +274,41 @@ export class SiuService {
private parseInscripcionesActivasHtml(html: string): any[] {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Buscamos los elementos que contienen los nombres de las materias (según el HTML del usuario)
const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td'));
// Buscamos los elementos que contienen las inscripciones activas
const rows = Array.from(doc.querySelectorAll('.js-mis-inscripciones td, .inscripciones-lista td, .cuadro_materias td, table.tabla_cursada td'));
return rows.map(td => {
const results: any[] = [];
for (const td of rows) {
const text = td.textContent?.trim() || '';
// Formato esperado: "Nombre Materia - ASIG00125"
const parts = text.split(' - ');
if (parts.length >= 2) {
if (!text || text.length < 5) continue;
// Caso 1: "Nombre Materia - ASIG00127-E1-TD"
if (text.includes(' - ')) {
const parts = text.split(' - ');
const codigo = parts[parts.length - 1].trim();
const nombre = parts.slice(0, -1).join(' - ').trim();
return { nombre, codigo };
if (codigo.toUpperCase().includes('ASIG') || codigo.length >= 4) {
results.push({ nombre, codigo });
continue;
}
}
return null;
}).filter(item => item !== null);
// Caso 2: "Nombre Materia (ASIG00127-E1-TD)"
const matchParen = text.match(/^(.*?)\s*\((ASIG[^\)]+)\)/i);
if (matchParen) {
results.push({ nombre: matchParen[1].trim(), codigo: matchParen[2].trim() });
continue;
}
// Caso 3: contiene código ASIG embebido
const matchAsig = text.match(/(ASIG[A-Z0-9\-_]+)/i);
if (matchAsig) {
const codigo = matchAsig[1].trim();
const nombre = text.replace(codigo, '').replace(/[-–()]/g, '').trim();
results.push({ nombre, codigo });
}
}
return results;
}
private parseHtmlSubjects(html: string): any[] {