Files
aulas-unicaba-mvp/src/app/services/data.ts
T

246 lines
7.9 KiB
TypeScript

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin, from, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';
import { Preferences } from '@capacitor/preferences';
import { LocalNotifications } from '@capacitor/local-notifications';
export interface ClaseHorario {
codigo: string;
asignatura: string;
aula: string;
carrera: string;
horario: string;
dia?: string;
link?: string;
}
@Injectable({
providedIn: 'root',
})
export class DataService {
private readonly BASE_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSc_T_BQjbn3uPelioCgx52UM5Py-qNhJN0TYPd1kmsN5jdb3Q8rAaIvNMF_2ZTzQt6bH--yIWQKrKR/pub?single=true&output=csv';
private readonly CACHE_KEY = 'cache_horarios';
private readonly CACHE_VERSION = '1.1';
private readonly CACHE_FAVS_KEY = 'favoritos';
private readonly SHEETS = [
{ name: 'Lunes', gid: '1915752353' },
{ name: 'Martes', gid: '466778263' },
{ name: 'Miércoles', gid: '615483345' },
{ name: 'Jueves', gid: '682416571' },
{ name: 'Viernes', gid: '1162906711' },
{ name: 'Sábado', gid: '752305601' },
];
constructor(private http: HttpClient) { }
/**
* Obtiene horarios del servidor y actualiza la caché.
* Si hay fallos de red, se maneja en el componente llamando a getCachedHorarios.
*/
public getHorarios(): Observable<ClaseHorario[]> {
const requests = this.SHEETS.map(sheet =>
this.http.get(`${this.BASE_CSV_URL}&gid=${sheet.gid}`, { responseType: 'text' }).pipe(
map(csvData => this.parseCsv(csvData, sheet.name))
)
);
return forkJoin(requests).pipe(
map(results => results.reduce((acc, val) => acc.concat(val), []))
);
}
/**
* Guarda los nuevos datos en caché y detecta cambios de aula para notificaciones
*/
public async syncWithCache(newData: ClaseHorario[], favoritos: string[]) {
const cachedData = await this.getCachedHorarios();
if (cachedData.length > 0) {
this.checkRoomChanges(cachedData, newData, favoritos);
}
await Preferences.set({
key: this.CACHE_KEY,
value: JSON.stringify({
version: this.CACHE_VERSION,
data: newData,
timestamp: Date.now()
})
});
}
public async getCachedHorarios(): Promise<ClaseHorario[]> {
const { value } = await Preferences.get({ key: this.CACHE_KEY });
if (!value) return [];
try {
const cached = JSON.parse(value);
if (cached.version === this.CACHE_VERSION) {
return cached.data || [];
} else if (Array.isArray(cached)) {
return cached; // Compatibilidad con caché vieja (v1.0)
}
return [];
} catch (e) {
return [];
}
}
public async getFavorites(): Promise<string[]> {
const { value } = await Preferences.get({ key: this.CACHE_FAVS_KEY });
if (!value) return [];
try {
return JSON.parse(value) || [];
} catch (e) {
return [];
}
}
private async checkRoomChanges(oldData: ClaseHorario[], newData: ClaseHorario[], favoritos: string[]) {
for (const item of newData) {
if (favoritos.includes(item.codigo)) {
const oldItem = oldData.find(o =>
o.codigo === item.codigo &&
o.dia === item.dia &&
o.horario === item.horario
);
if (oldItem) {
const roomChanged = oldItem.aula !== item.aula;
const linkChanged = oldItem.link !== item.link;
if (roomChanged || linkChanged) {
await this.notifyChange(item, roomChanged, linkChanged);
}
}
}
}
}
private async notifyChange(item: ClaseHorario, roomChanged: boolean, linkChanged: boolean) {
try {
let body = '';
if (roomChanged && linkChanged) {
body = `El aula cambió a: ${item.aula} y el enlace fue actualizado.`;
} else if (roomChanged) {
body = `El aula ha cambiado al aula: ${item.aula}`;
} else if (linkChanged) {
body = item.link ? 'Hay un nuevo enlace disponible para tu clase.' : 'El enlace de la clase virtual ha sido removido.';
}
await LocalNotifications.schedule({
notifications: [
{
title: `Cambio en tu clase: ${item.asignatura}`,
body: body,
id: Math.floor(Math.random() * 10000),
schedule: { at: new Date(Date.now() + 500) },
extra: { codigo: item.codigo }
}
]
});
} catch (e) {
console.error('Error al enviar notificación:', e);
}
}
private parseCsv(csv: string, dia: string): ClaseHorario[] {
const result: ClaseHorario[] = [];
const rows: string[][] = [];
let currentRow: string[] = [];
let currentCell = '';
let inQuotes = false;
for (let i = 0; i < csv.length; i++) {
const char = csv[i];
const nextChar = csv[i + 1];
if (inQuotes) {
if (char === '"' && nextChar === '"') {
currentCell += '"';
i++;
} else if (char === '"') {
inQuotes = false;
} else {
currentCell += char;
}
} else {
if (char === '"') {
inQuotes = true;
} else if (char === ',') {
currentRow.push(currentCell);
currentCell = '';
} else if (char === '\n' || (char === '\r' && nextChar === '\n')) {
currentRow.push(currentCell);
rows.push(currentRow);
currentRow = [];
currentCell = '';
if (char === '\r') i++;
} else if (char !== '\r') {
currentCell += char;
}
}
}
if (currentCell !== '' || currentRow.length > 0) {
currentRow.push(currentCell);
rows.push(currentRow);
}
let headerRowIndex = -1;
let colOffsetX = 0;
let linkColIndex = -1; // Índice absoluto de la columna Link (si existe)
let horarioColIndex = -1; // Índice absoluto de la columna Horario
for (let r = 0; r < rows.length; r++) {
const row = rows[r];
for (let c = 0; c < row.length; c++) {
const cellLower = row[c].trim().toLowerCase();
if (cellLower.includes('codigo') || cellLower.includes('código')) {
headerRowIndex = r;
colOffsetX = c;
break;
}
}
if (headerRowIndex !== -1) break;
}
if (headerRowIndex === -1) return [];
// Detectar columnas dinámicamente leyendo la fila de encabezados
const headerRow = rows[headerRowIndex];
for (let c = colOffsetX; c < headerRow.length; c++) {
const cellLower = headerRow[c].trim().toLowerCase();
if (cellLower === 'link' || cellLower.startsWith('http')) {
linkColIndex = c;
}
if (cellLower.includes('horario')) {
horarioColIndex = c;
}
}
// Si no encontramos la columna horario explícitamente, usamos el offset clásico
if (horarioColIndex === -1) {
horarioColIndex = linkColIndex !== -1 ? colOffsetX + 5 : colOffsetX + 4;
}
for (let r = headerRowIndex + 1; r < rows.length; r++) {
const row = rows[r];
if (row.length >= colOffsetX + 4) {
const codigo = (row[colOffsetX] || '').trim().replace(/\n/g, ' - ');
const asignatura = (row[colOffsetX + 1] || '').trim().replace(/\n/g, ' ');
const aula = (row[colOffsetX + 2] || '').trim();
const carrera = (row[colOffsetX + 3] || '').trim();
const horario = (row[horarioColIndex] || '').trim().replace(/([0-9])\s*(hs|horas|h)\b\.?/gi, '$1').trim();
const link = linkColIndex !== -1 ? (row[linkColIndex] || '').trim() : undefined;
if (codigo && asignatura && carrera &&
!codigo.toLowerCase().includes('codigo') &&
!codigo.toLowerCase().includes('código') &&
!codigo.toLowerCase().startsWith('turno')) {
result.push({ codigo, asignatura, aula, carrera, horario, dia, link: link || undefined });
}
}
}
return result;
}
}