import { Injectable } from '@angular/core'; import { CapacitorHttp, HttpResponse } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { FileOpener } from '@capacitor-community/file-opener'; 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'; async getAcademicHistory(dni: string, pass: string, carreraId: string = '30'): Promise { try { await this.authenticateSiu(dni, pass); await this.selectCareer(carreraId); // 3. OBTENER PLAN DE ESTUDIO const resPlan: HttpResponse = await CapacitorHttp.get({ url: `${this.BASE_URL}/plan_estudio`, }); // 4. PARSEO DEL JSON EMBEBIDO 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 Academic History:', error); throw error; } } async getInscripcionesActivas(dni: string, pass: string, carreraId: string = '30'): Promise { try { await this.authenticateSiu(dni, pass); await this.selectCareer(carreraId); // OBTENER PÁGINA DE CURSADA const resCursada: HttpResponse = await CapacitorHttp.get({ url: `${this.BASE_URL}/cursada`, }); const rawHtml = resCursada.data; // Buscamos el bloque on_arrival que contiene "info_materia" (donde están las inscripciones) const regex = /on_arrival\((\{.*?"id":"info_materia".*?\})\);/s; const match = rawHtml.match(regex); if (match && match[1]) { const jsonData = JSON.parse(match[1]); const contentHtml = jsonData.content; return this.parseInscripcionesActivasHtml(contentHtml); } return []; // Quizás no tiene inscripciones activas } catch (error) { console.error('Error SIU Active Inscriptions:', error); return []; } } private async authenticateSiu(dni: string, pass: string): Promise { console.log('Autenticando en SIU...'); 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' } }); } private async selectCareer(carreraId: string): Promise { console.log('Seleccionando carrera:', carreraId); await CapacitorHttp.get({ url: `${this.BASE_URL}/acceso/cambiar_carrera?id=${carreraId}&op=inicio_alumno`, }); // Hit inicio_alumno to ensure context is established await CapacitorHttp.get({ url: `${this.BASE_URL}/inicio_alumno`, }); } private extractTobaContent(html: string, pageletId: string = 'pagelet'): string { const regexes = [ /kernel\.renderer\.on_arrival\((\{.*?\})\);/gs, /on_arrival\((\{.*?\})\);/gs ]; for (const regex of regexes) { let match; // Reset regex index for global searches regex.lastIndex = 0; while ((match = regex.exec(html)) !== null) { try { const jsonData = JSON.parse(match[1]); if (jsonData.id === pageletId || jsonData.info?.id === pageletId) { return jsonData.content || ''; } } catch (e) { continue; } } } return ''; } async downloadCertificate(dni: string, pass: string): Promise<{ base64: string, filename: string }> { console.log('Iniciando descarga de certificado para:', dni); try { await this.authenticateSiu(dni, pass); await this.selectCareer('30'); console.log('Login y selección de carrera (ID 30) listos'); // 1. VALIDAR SI EXISTE UNO VIGENTE (< 30 DÍAS) const resSolicitudes: HttpResponse = await CapacitorHttp.get({ url: `${this.BASE_URL}/solicitudes`, }); console.log('Página de solicitudes obtenida'); const contentHtmlSolicitudes = this.extractTobaContent(resSolicitudes.data, 'pagelet'); if (contentHtmlSolicitudes) { const existingCert = this.findValidCertificate(contentHtmlSolicitudes); if (existingCert) { console.log('Certificado vigente encontrado:', existingCert.filename); return await this.downloadFile(existingCert.url, existingCert.filename); } } // 2. GENERAR NUEVA SOLICITUD (HEADLESS) console.log('No hay certificado vigente o no se pudo leer la tabla. Intentando generar uno nuevo...'); const resGestion: HttpResponse = await CapacitorHttp.get({ url: `${this.BASE_URL}/solicitudes/solicitud_gestion`, }); const contentHtmlGestion = this.extractTobaContent(resGestion.data, 'pagelet'); if (!contentHtmlGestion) { console.error('HTML completo de gestión no contiene on_arrival de pagelet. Muestra:', resGestion.data.substring(0, 1000)); throw new Error('No se pudo localizar el generador (content missing)'); } const parser = new DOMParser(); const doc = parser.parseFromString(contentHtmlGestion, 'text/html'); const iframe = doc.querySelector('iframe#frameSolicitud') as HTMLIFrameElement; let iframeSrc = iframe?.getAttribute('src'); if (!iframeSrc) { throw new Error('No se pudo localizar el generador (iframe src missing)'); } console.log('Iframe localizado, src:', iframeSrc); // Asegurar que la URL del iframe es completa if (!iframeSrc.startsWith('http')) { const baseUrlParsed = new URL(this.BASE_URL); iframeSrc = `${baseUrlParsed.origin}${iframeSrc.startsWith('/') ? '' : '/'}${iframeSrc}`; } // Trigger the print event via URL parameters const printUrl = iframeSrc.includes('?') ? `${iframeSrc}&toba_ei_notificar=imprimir` : `${iframeSrc}?toba_ei_notificar=imprimir`; console.log('Solicitando generación de PDF:', printUrl); return await this.downloadFile(printUrl, `Certificado_Alumno_Regular_${new Date().getTime()}.pdf`); } catch (error) { console.error('Error detallado en SIU Certificate:', error); throw error; } } private findValidCertificate(html: string): { url: string, filename: string } | null { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const rows = Array.from(doc.querySelectorAll('tr')); for (const row of rows) { const cells = Array.from(row.querySelectorAll('td')); if (cells.length >= 3) { const tipo = cells[0].textContent?.trim().toLowerCase() || ''; const presentarA = cells[2].textContent?.trim().toLowerCase() || ''; // Identificar constancia de alumno regular if (tipo.includes('alumno regular')) { const fechaStr = cells[1].textContent?.trim() || ''; console.log(`Revisando: ${tipo} | Para: ${presentarA} | Fecha: ${fechaStr}`); if (this.isWithin30Days(fechaStr)) { // El usuario indico que si dice "Quien corresponda" o similares (como "Google LLC") sirve. // Solo descartamos si es para un destinatario muy especifico que no sea el solicitado. const downloadLink = row.querySelector('a.descargar_cert, a[href*="descargar_comprobante"]') as HTMLAnchorElement; if (downloadLink) { return { url: downloadLink.getAttribute('href') || '', filename: `Certificado_SIU_${presentarA.replace(/\s+/g, '_')}_${fechaStr.replace(/\//g, '-')}.pdf` }; } } } } } return null; } private isWithin30Days(fechaStr: string): boolean { // Formato esperado: DD/MM/YYYY const parts = fechaStr.split('/'); if (parts.length !== 3) return false; const date = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0])); const diff = new Date().getTime() - date.getTime(); const days = diff / (1000 * 60 * 60 * 24); return days < 30; } private async downloadFile(url: string, filename: string): Promise<{ base64: string, filename: string }> { // Si la URL es relativa, completarla const fullUrl = url.startsWith('http') ? url : `${this.BASE_URL}/${url.startsWith('/') ? url.substring(1) : url}`; const response: HttpResponse = await CapacitorHttp.get({ url: fullUrl, responseType: 'blob' }); return { base64: response.data, // CapacitorHttp con responseType blob devuelve base64 string filename }; } async saveAndOpenFile(base64Data: string, filename: string): Promise { try { const savedFile = await Filesystem.writeFile({ path: `Certificados/${filename}`, data: base64Data, directory: Directory.Documents, recursive: true }); await FileOpener.open({ filePath: savedFile.uri, contentType: 'application/pdf', }); } catch (error) { console.error('Error saving or opening file:', error); throw error; } } 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')); return rows.map(td => { const text = td.textContent?.trim() || ''; // Formato esperado: "Nombre Materia - ASIG00125" const parts = text.split(' - '); if (parts.length >= 2) { const codigo = parts[parts.length - 1].trim(); const nombre = parts.slice(0, -1).join(' - ').trim(); return { nombre, codigo }; } return null; }).filter(item => item !== null); } 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); } }