v1.2.3: Fix SIU SSL, matching de comisiones/equipos, credencial QR ampliada y actualizacion de beneficios
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user