From 89926057d80d044f121a0cd4488da5163fe41741 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 09:57:18 -0600 Subject: [PATCH 1/8] initial add cache commit --- src/firebase/cache/cache.js | 313 ++++++++ src/firebase/cache/config.js | 72 ++ src/firebase/db/annoucement.cached.js | 100 +++ src/firebase/db/annoucement.js | 417 +--------- src/firebase/db/annoucement.legacy.js | 416 ++++++++++ src/firebase/db/asesorias.cached.js | 203 +++++ src/firebase/db/asesorias.js | 456 +---------- src/firebase/db/asesorias.legacy.js | 455 +++++++++++ src/firebase/db/attendance.cached.js | 99 +++ src/firebase/db/attendance.js | 220 +----- src/firebase/db/attendance.legacy.js | 219 ++++++ src/firebase/db/campuses.cached.js | 29 + src/firebase/db/campuses.js | 18 +- src/firebase/db/maeteca.cached.js | 419 ++++++++++ src/firebase/db/maeteca.js | 407 +--------- src/firebase/db/majors.cached.js | 27 + src/firebase/db/majors.js | 18 +- src/firebase/db/subjects.cached.js | 45 ++ src/firebase/db/subjects.js | 33 +- src/firebase/db/users.cached.js | 1038 +++++++++++++++++++++++++ src/firebase/db/users.js | 1035 +----------------------- src/firebase/img/users.js | 71 +- src/views/AdminAsesorias.vue | 2 +- src/views/AdminFunciones.vue | 2 +- src/views/pages/Landing.vue | 2 +- 25 files changed, 3485 insertions(+), 2631 deletions(-) create mode 100644 src/firebase/cache/cache.js create mode 100644 src/firebase/cache/config.js create mode 100644 src/firebase/db/annoucement.cached.js create mode 100644 src/firebase/db/annoucement.legacy.js create mode 100644 src/firebase/db/asesorias.cached.js create mode 100644 src/firebase/db/asesorias.legacy.js create mode 100644 src/firebase/db/attendance.cached.js create mode 100644 src/firebase/db/attendance.legacy.js create mode 100644 src/firebase/db/campuses.cached.js create mode 100644 src/firebase/db/maeteca.cached.js create mode 100644 src/firebase/db/majors.cached.js create mode 100644 src/firebase/db/subjects.cached.js create mode 100644 src/firebase/db/users.cached.js diff --git a/src/firebase/cache/cache.js b/src/firebase/cache/cache.js new file mode 100644 index 000000000..cc4d6929a --- /dev/null +++ b/src/firebase/cache/cache.js @@ -0,0 +1,313 @@ +import { Timestamp } from 'firebase/firestore'; + +const DB_NAME = 'maesmx-cache'; +const STORE_NAME = 'entries'; + +const memoryCache = new Map(); +const inFlightRequests = new Map(); + +function isBrowser() { + return typeof window !== 'undefined'; +} + +function isTimestampLike(value) { + return ( + value && + typeof value === 'object' && + typeof value.seconds === 'number' && + typeof value.nanoseconds === 'number' && + typeof value.toDate === 'function' + ); +} + +function serializeValue(value) { + if (value === undefined) { + return { __cacheType: 'undefined' }; + } + + if (value === null || typeof value !== 'object') { + return value; + } + + if (value instanceof Date) { + return { __cacheType: 'Date', value: value.toISOString() }; + } + + if (isTimestampLike(value)) { + return { + __cacheType: 'Timestamp', + seconds: value.seconds, + nanoseconds: value.nanoseconds + }; + } + + if (Array.isArray(value)) { + return value.map((item) => serializeValue(item)); + } + + const serialized = {}; + Object.entries(value).forEach(([key, nestedValue]) => { + serialized[key] = serializeValue(nestedValue); + }); + return serialized; +} + +function deserializeValue(value) { + if (value === null || typeof value !== 'object') { + return value; + } + + if (Array.isArray(value)) { + return value.map((item) => deserializeValue(item)); + } + + if (value.__cacheType === 'undefined') { + return undefined; + } + + if (value.__cacheType === 'Date') { + return new Date(value.value); + } + + if (value.__cacheType === 'Timestamp') { + return new Timestamp(value.seconds, value.nanoseconds); + } + + const deserialized = {}; + Object.entries(value).forEach(([key, nestedValue]) => { + deserialized[key] = deserializeValue(nestedValue); + }); + return deserialized; +} + +function cloneValue(value) { + return deserializeValue(serializeValue(value)); +} + +function isExpired(entry) { + return typeof entry?.expiresAt === 'number' && entry.expiresAt <= Date.now(); +} + +function normalizeTags(tags = []) { + return Array.from(new Set(tags.filter(Boolean))); +} + +function createEntry(key, value, { ttlMs = 0, tags = [] } = {}) { + return { + key, + value: cloneValue(value), + expiresAt: ttlMs > 0 ? Date.now() + ttlMs : Number.POSITIVE_INFINITY, + tags: normalizeTags(tags), + updatedAt: Date.now() + }; +} + +function getMemoryEntry(key) { + const entry = memoryCache.get(key); + if (!entry) { + return null; + } + + if (isExpired(entry)) { + memoryCache.delete(key); + return null; + } + + return entry; +} + +function setMemoryEntry(key, entry) { + memoryCache.set(key, { + ...entry, + value: cloneValue(entry.value), + tags: normalizeTags(entry.tags) + }); +} + +async function openDb() { + if (!isBrowser() || !('indexedDB' in window)) { + return null; + } + + return await new Promise((resolve, reject) => { + const request = window.indexedDB.open(DB_NAME, 1); + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'key' }); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +async function readPersistentEntry(key) { + const db = await openDb(); + if (!db) { + return null; + } + + return await new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(key); + + request.onsuccess = () => resolve(request.result ?? null); + request.onerror = () => reject(request.error); + }); +} + +async function writePersistentEntry(entry) { + const db = await openDb(); + if (!db) { + return; + } + + const record = { + ...entry, + value: serializeValue(entry.value), + tags: normalizeTags(entry.tags) + }; + + await new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(record); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +async function deletePersistentEntry(key) { + const db = await openDb(); + if (!db) { + return; + } + + await new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.delete(key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +async function getAllPersistentEntries() { + const db = await openDb(); + if (!db) { + return []; + } + + return await new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.getAll(); + + request.onsuccess = () => resolve(request.result ?? []); + request.onerror = () => reject(request.error); + }); +} + +async function hydratePersistentEntry(key) { + const entry = await readPersistentEntry(key); + if (!entry) { + return null; + } + + const hydratedEntry = { + ...entry, + value: deserializeValue(entry.value), + tags: normalizeTags(entry.tags) + }; + + if (isExpired(hydratedEntry)) { + await deletePersistentEntry(key); + return null; + } + + setMemoryEntry(key, hydratedEntry); + return hydratedEntry; +} + +export async function setCachedValue(key, value, { ttlMs = 0, tags = [], persist = false } = {}) { + const entry = createEntry(key, value, { ttlMs, tags }); + setMemoryEntry(key, entry); + + if (persist) { + await writePersistentEntry(entry); + } + + return cloneValue(entry.value); +} + +export async function withCache( + key, + { ttlMs = 0, tags = [], persist = false, forceRefresh = false, cacheNull = true } = {}, + loader +) { + if (!forceRefresh) { + const memoryEntry = getMemoryEntry(key); + if (memoryEntry) { + return cloneValue(memoryEntry.value); + } + + if (persist) { + const persistentEntry = await hydratePersistentEntry(key); + if (persistentEntry) { + return cloneValue(persistentEntry.value); + } + } + } + + if (inFlightRequests.has(key)) { + return cloneValue(await inFlightRequests.get(key)); + } + + const request = (async () => { + const freshValue = await loader(); + if (freshValue !== null || cacheNull) { + await setCachedValue(key, freshValue, { ttlMs, tags, persist }); + } + return freshValue; + })().finally(() => { + inFlightRequests.delete(key); + }); + + inFlightRequests.set(key, request); + return cloneValue(await request); +} + +export async function invalidateCacheKey(key) { + memoryCache.delete(key); + await deletePersistentEntry(key); +} + +export async function invalidateCacheTags(tags = []) { + const wantedTags = normalizeTags(tags); + if (wantedTags.length === 0) { + return; + } + + for (const [key, entry] of memoryCache.entries()) { + if (entry.tags?.some((tag) => wantedTags.includes(tag))) { + memoryCache.delete(key); + } + } + + const persistentEntries = await getAllPersistentEntries(); + const keysToDelete = persistentEntries + .filter((entry) => entry.tags?.some((tag) => wantedTags.includes(tag))) + .map((entry) => entry.key); + + await Promise.all(keysToDelete.map((key) => deletePersistentEntry(key))); +} + +export function clearMemoryCache() { + memoryCache.clear(); +} diff --git a/src/firebase/cache/config.js b/src/firebase/cache/config.js new file mode 100644 index 000000000..322578b8e --- /dev/null +++ b/src/firebase/cache/config.js @@ -0,0 +1,72 @@ +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +export const CACHE_TTL_MS = { + CURRENT_USER: 5 * MINUTE, + USER: 15 * MINUTE, + PROFILE_PICTURE: 30 * DAY, + MAE_DIRECTORY: 7 * DAY, + ACTIVE_MAES: 30 * SECOND, + SUBJECTS: 7 * DAY, + MAJORS: 7 * DAY, + CAMPUSES: 7 * DAY, + ANNOUNCEMENTS: 5 * MINUTE, + GROUP_ANNOUNCEMENTS: 1 * MINUTE, + ANNOUNCEMENTS_EDIT: 1 * MINUTE, + ATTENDANCE_TODAY: 30 * SECOND, + ATTENDANCE_DAY: 10 * MINUTE, + ATTENDANCE_RANGE: 10 * MINUTE, + ASESORIAS: 5 * MINUTE, + LEADERBOARD: 5 * MINUTE, + VIDEOS: 1 * DAY +}; + +export const CACHE_TAGS = { + USERS: 'users', + USER_DETAILS: 'users:details', + CURRENT_USER: 'users:current', + MAES: 'users:maes', + ACTIVE_MAES: 'users:active', + PROFILE_PICTURES: 'users:photos', + SUBJECTS: 'subjects', + MAJORS: 'majors', + CAMPUSES: 'campuses', + ANNOUNCEMENTS: 'announcements', + GROUP_ANNOUNCEMENTS: 'announcements:group', + ATTENDANCE: 'attendance', + ASESORIAS: 'asesorias', + LEADERBOARD: 'leaderboard', + VIDEOS: 'videos' +}; + +export const userTag = (uid) => `user:${uid}`; +export const attendanceDateTag = (dateString) => `attendance:${dateString}`; +export const profilePictureTag = (email) => `profile-picture:${email?.toLowerCase?.() ?? email}`; + +export const cacheKeys = { + currentUser: (uid) => `users:current:${uid}`, + userById: (uid) => `users:detail:${uid}`, + maeDirectory: () => 'users:mae-directory', + maesToday: (day) => `users:maes-today:${day}`, + activeMaes: (mode = 'basic') => `users:active:${mode}`, + subjects: () => 'reference:subjects', + majors: () => 'reference:majors', + campuses: () => 'reference:campuses', + announcementsVisible: () => 'announcements:visible', + announcementsEdit: () => 'announcements:edit', + announcementsGroup: () => 'announcements:group', + attendanceToday: (dateString) => `attendance:today:${dateString}`, + attendanceByDate: (dateString) => `attendance:date:${dateString}`, + attendanceStudent: (uid, dateString) => `attendance:student:${uid}:${dateString}`, + attendanceRange: (startDate, endDate) => `attendance:range:${startDate}:${endDate}`, + asesoriasRange: (startDate, endDate) => `asesorias:range:${startDate ?? 'all'}:${endDate ?? 'all'}`, + asesoriasSemesterByPeer: (uid, semesterKey) => `asesorias:semester:${uid}:${semesterKey}`, + asesoriasPendingRating: (uidUser, uidPeer = 'all') => `asesorias:pending-rating:${uidUser}:${uidPeer}`, + leaderboard: () => 'users:leaderboard', + videosAll: () => 'videos:all', + videoById: (id) => `videos:detail:${id}`, + videosByRelated: (related) => `videos:related:${related ?? 'all'}`, + profilePicture: (email) => `users:profile-picture:${email?.toLowerCase?.() ?? email}` +}; diff --git a/src/firebase/db/annoucement.cached.js b/src/firebase/db/annoucement.cached.js new file mode 100644 index 000000000..2b32ebac4 --- /dev/null +++ b/src/firebase/db/annoucement.cached.js @@ -0,0 +1,100 @@ +import * as announcementDb from './annoucement.legacy'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +async function invalidateAnnouncementCaches() { + await invalidateCacheTags([CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS]); +} + +export async function saveAnnouncement(announcementData, selectedFile) { + const result = await announcementDb.saveAnnouncement(announcementData, selectedFile); + await invalidateAnnouncementCaches(); + return result; +} + +export async function getAnnouncementsEdit(options = {}) { + return await withCache( + cacheKeys.announcementsEdit(), + { + ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS_EDIT, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsEdit() + ); +} + +export async function getAnnouncements(options = {}) { + return await withCache( + cacheKeys.announcementsVisible(), + { + ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncements() + ); +} + +export async function getAnnouncementsGrupales(options = {}) { + return await withCache( + cacheKeys.announcementsGroup(), + { + ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsGrupales() + ); +} + +export async function addUserToPreregsiter(announcementId, user) { + const result = await announcementDb.addUserToPreregsiter(announcementId, user); + await invalidateAnnouncementCaches(); + return result; +} + +export const processAsistence = announcementDb.processAsistence; +export const processConfirms = announcementDb.processConfirms; + +export async function updateUserAsistence(announcementId, userId) { + const result = await announcementDb.updateUserAsistence(announcementId, userId); + await invalidateAnnouncementCaches(); + return result; +} + +export const addExtraVariables = announcementDb.addExtraVariables; + +export async function getAnnouncementsAllGrupales(options = {}) { + return await withCache( + 'announcements:group:all', + { + ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsAllGrupales() + ); +} + +export async function deleteAnnouncementById(id) { + const result = await announcementDb.deleteAnnouncementById(id); + await invalidateAnnouncementCaches(); + return result; +} + +export async function updateAnnouncement(announcementId, updatedData) { + const result = await announcementDb.updateAnnouncement(announcementId, updatedData); + await invalidateAnnouncementCaches(); + return result; +} + +export async function toggleVisibilityById(id) { + const result = await announcementDb.toggleVisibilityById(id); + await invalidateAnnouncementCaches(); + return result; +} diff --git a/src/firebase/db/annoucement.js b/src/firebase/db/annoucement.js index 851913ca8..aa5d17894 100644 --- a/src/firebase/db/annoucement.js +++ b/src/firebase/db/annoucement.js @@ -1,416 +1 @@ -import { firestoreDB } from "../../main"; -import { - addDoc, - collection, - query, - getDocs, - where, - updateDoc, - doc, - getDoc, - deleteDoc -} from 'firebase/firestore'; -import { addAnnoucement } from "../img/users"; -import { - updatePoints -} from './users'; - -export async function saveAnnouncement(announcementData, selectedFile) { - try { - console.log(announcementData.maesAsignados) - let imageUrl = ''; - - if (selectedFile) { - const filePath = `announcements/${announcementData.type}/${selectedFile.name}`; - imageUrl = await addAnnoucement(selectedFile, filePath); - } - const docRef = await addDoc(collection(firestoreDB, 'announcements'), { - ...announcementData, - imageUrl, - preregister: {}, - asistence: {}, - createdAt: new Date(), - visible: true - }); - - return docRef.id; - } catch (error) { - console.error('Error al guardar el anuncio:', error); - throw error; - } -} - -export async function getAnnouncementsEdit() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const querySnapshot = await getDocs(query(announcementsCollection)); - - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - })) - .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt )); - - - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function getAnnouncements() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - const q = query(announcementsCollection, where('visible', '==', true)); - const querySnapshot = await getDocs(q); - const now = new Date(); - console.log(querySnapshot.docs) - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - })) - .filter(announcement => { - // Filtrar por fecha válida - if (announcement.dateTime) { - const dateTime = announcement.dateTime.seconds - ? new Date(announcement.dateTime.seconds * 1000) - : new Date(announcement.dateTime); - if (dateTime < now && dateTime.toDateString() !== now.toDateString()) { - return false; - } - } - - // Filtrar por visible o tipo Especial - const isVisible = announcement.visible === true; - const isSpecial = announcement.id === undefined; - console.log(announcement.type) - console.log(announcement.visible) - return isVisible || isSpecial; - }) - .sort((a, b) => { - const dateA = a.createdAt.seconds ? new Date(a.createdAt.seconds * 1000) : new Date(a.createdAt); - const dateB = b.createdAt.seconds ? new Date(b.createdAt.seconds * 1000) : new Date(b.createdAt); - return dateA - dateB; - }); - console.log(announcements) - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function getAnnouncementsGrupales() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const q = query( - announcementsCollection, - where('type', '==', 'Asesoría'), - where('visible', '==', true) - ); - - const querySnapshot = await getDocs(q); - - const now = new Date(); - console.log(now); - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - dateTime: doc.data().dateTime.toDate(), - })) - .filter(ann => { - return ann.dateTime >= now || ann.dateTime.toDateString() === now.toDateString(); - }) - .sort((a, b) => a.createdAt.seconds - b.createdAt.seconds); // Ordenar por fecha de creación - - console.log(announcements); - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function addUserToPreregsiter(announcementId, user) { - try { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - if (!announcementSnapshot.exists()) { - throw new Error(`El anuncio con ID ${announcementId} no existe.`); - } - - const announcementData = announcementSnapshot.data(); - - const currentPreregs = announcementData.preregister || {}; - if (currentPreregs[user.uid]) { - throw new Error('Usuario ya registrado'); - } - const updatedPreregs = { - ...currentPreregs, - [user.uid]: user, - }; - - const currentAsistence = announcementData.asistence || {}; - const updatedAsistence = { - ...currentAsistence, - [user.uid]: false, - }; - - await updateDoc(announcementRef, { - preregister: updatedPreregs, - asistence: updatedAsistence, - }); - - console.log(`Usuario ${user.uid} agregado exitosamente a preregister y asistencia.`); - } catch (error) { - console.error('Error añadiendo usuario a preregister:', error); - throw error; - } -} - - -export async function processAsistence(announcementId) { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - const data = announcementSnapshot.data(); - - const preregister = data.preregister || {}; - const asistence = data.asistence || {}; - const dateTime = data.dateTime || ''; - - const preregisterKeys = Object.keys(preregister); - if (preregisterKeys.length === 0) { - console.log("No preregister data found."); - return []; - } - - const result = preregisterKeys.map(uid => { - const user = preregister[uid]; - - return { - uid: uid, - dateTime: dateTime, - name: user.name || '', - career: user.career || '', - area: user.area || '', - campus: user.campus || '', - asistence: asistence[uid] || false - }; - }); - - console.log("Result:", result); - return result; -} - - -export async function updateUserAsistence(announcementId, userId) { - try { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - - const announcementSnapshot = await getDoc(announcementRef); - if (!announcementSnapshot.exists()) { - throw new Error(`El anuncio con ID ${announcementId} no existe.`); - } - - const announcementData = announcementSnapshot.data(); - const currentAsistence = announcementData.asistence || {}; - const maesAsignados = announcementData.maesAsignados || []; - - const newAsistenceStatus = !currentAsistence[userId]; - const updatedAsistence = { - ...currentAsistence, - [userId]: newAsistenceStatus, - }; - - const totalMaes = maesAsignados.length; - - if (totalMaes > 0) { - const pointsPerMae = 50 / totalMaes; - - - for (const mae of maesAsignados) { - if (newAsistenceStatus) { - - await updatePoints(mae.uid, pointsPerMae); - console.log(`Puntos distribuidos a ${mae.name}: +${pointsPerMae}`); - } else { - - await updatePoints(mae.uid, -pointsPerMae); - console.log(`Puntos distribuidos a ${mae.name}: -${pointsPerMae}`); - } - } - } else { - console.log('No hay MAEs asignados para asignar puntos.'); - } - - await updateDoc(announcementRef, { - asistence: updatedAsistence, - }); - - console.log(`Asistencia para el usuario ${userId} actualizada exitosamente a ${newAsistenceStatus}.`); - } catch (error) { - console.error('Error actualizando la asistencia del usuario:', error); - throw error; - } -} - - -export async function processConfirms(announcementId) { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - const data = announcementSnapshot.data(); - - const preregister = data.preregister || {}; - const asistence = data.asistence || {}; - const dateTime = data.dateTime || ''; - - const preregisterKeys = Object.keys(preregister); - if (preregisterKeys.length === 0) { - console.log("No preregister data found."); - return []; - } - - const result = preregisterKeys - .filter(uid => asistence[uid] === true) - .map(uid => { - const user = preregister[uid]; - console.log("Processing user:", user); - - return { - uid: uid, - dateTime: dateTime, - name: user.name || '', - career: user.career || '', - area: user.area || '', - campus: user.campus || '', - asistence: true - }; - }); - return result; -} - - -export async function addExtraVariables() { - try { - const usersRef = collection(firestoreDB, 'announcements'); - const querySnapshot = await getDocs(usersRef); - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - return updateDoc(userRef, { - maesAsignados: [] - }); - - }); - - await Promise.all(promises); - - console.log("Background have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding background to eligible users: ", error); - throw error; - } -} - -export async function getAnnouncementsAllGrupales() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const q = query( - announcementsCollection, - where('type', '==', 'Asesoría') - ); - - const querySnapshot = await getDocs(q); - - const now = new Date(); - const announcements = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data(), - dateTime: doc.data().dateTime.toDate(), - })); - - const futureAnnouncements = announcements - .filter(announcement => - announcement.dateTime > now || - announcement.dateTime.toDateString() === now.toDateString() - ) - .sort((a, b) => a.dateTime - b.dateTime); - - const pastAnnouncements = announcements - .filter(announcement => - announcement.dateTime < now && - announcement.dateTime.toDateString() !== now.toDateString() - ) - .sort((a, b) => a.dateTime - b.dateTime); - - - const sortedAnnouncements = [...futureAnnouncements, ...pastAnnouncements]; - - - return sortedAnnouncements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - -export async function deleteAnnouncementById(id) { - try { - const announcementDocRef = doc(firestoreDB, "announcements", id); - - await deleteDoc(announcementDocRef); - - console.log(`Announcement with ID ${id} deleted successfully.`); - } catch (error) { - console.error(`Error deleting announcement with ID ${id}:`, error); - throw error; - } -} - -export async function updateAnnouncement(announcementId, updatedData) { - try { - const docRef = doc(firestoreDB, 'announcements', announcementId); - - await updateDoc(docRef, { - ...updatedData, - }); - - return docRef.id; - } catch (error) { - console.error('Error al actualizar el anuncio:', error); - throw error; - } -} - -export const toggleVisibilityById = async (id) => { - try { - console.log(id) - const dialogDocRef = doc(firestoreDB, 'announcements', id); - - const docSnap = await getDoc(dialogDocRef); - - if (docSnap.exists()) { - const currentVisibility = docSnap.data().visible - - await updateDoc(dialogDocRef, { - visible: !currentVisibility - }); - - console.log(`Visibilidad del diálogo con ID ${id} actualizada correctamente`); - } else { - console.log("El documento no existe"); - } - } catch (error) { - console.error(`Error al actualizar la visibilidad del diálogo con ID ${id}:`, error); - throw error; - } -}; +export * from './annoucement.cached'; diff --git a/src/firebase/db/annoucement.legacy.js b/src/firebase/db/annoucement.legacy.js new file mode 100644 index 000000000..851913ca8 --- /dev/null +++ b/src/firebase/db/annoucement.legacy.js @@ -0,0 +1,416 @@ +import { firestoreDB } from "../../main"; +import { + addDoc, + collection, + query, + getDocs, + where, + updateDoc, + doc, + getDoc, + deleteDoc +} from 'firebase/firestore'; +import { addAnnoucement } from "../img/users"; +import { + updatePoints +} from './users'; + +export async function saveAnnouncement(announcementData, selectedFile) { + try { + console.log(announcementData.maesAsignados) + let imageUrl = ''; + + if (selectedFile) { + const filePath = `announcements/${announcementData.type}/${selectedFile.name}`; + imageUrl = await addAnnoucement(selectedFile, filePath); + } + const docRef = await addDoc(collection(firestoreDB, 'announcements'), { + ...announcementData, + imageUrl, + preregister: {}, + asistence: {}, + createdAt: new Date(), + visible: true + }); + + return docRef.id; + } catch (error) { + console.error('Error al guardar el anuncio:', error); + throw error; + } +} + +export async function getAnnouncementsEdit() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const querySnapshot = await getDocs(query(announcementsCollection)); + + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + })) + .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt )); + + + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +export async function getAnnouncements() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + const q = query(announcementsCollection, where('visible', '==', true)); + const querySnapshot = await getDocs(q); + const now = new Date(); + console.log(querySnapshot.docs) + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + })) + .filter(announcement => { + // Filtrar por fecha válida + if (announcement.dateTime) { + const dateTime = announcement.dateTime.seconds + ? new Date(announcement.dateTime.seconds * 1000) + : new Date(announcement.dateTime); + if (dateTime < now && dateTime.toDateString() !== now.toDateString()) { + return false; + } + } + + // Filtrar por visible o tipo Especial + const isVisible = announcement.visible === true; + const isSpecial = announcement.id === undefined; + console.log(announcement.type) + console.log(announcement.visible) + return isVisible || isSpecial; + }) + .sort((a, b) => { + const dateA = a.createdAt.seconds ? new Date(a.createdAt.seconds * 1000) : new Date(a.createdAt); + const dateB = b.createdAt.seconds ? new Date(b.createdAt.seconds * 1000) : new Date(b.createdAt); + return dateA - dateB; + }); + console.log(announcements) + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +export async function getAnnouncementsGrupales() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const q = query( + announcementsCollection, + where('type', '==', 'Asesoría'), + where('visible', '==', true) + ); + + const querySnapshot = await getDocs(q); + + const now = new Date(); + console.log(now); + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + dateTime: doc.data().dateTime.toDate(), + })) + .filter(ann => { + return ann.dateTime >= now || ann.dateTime.toDateString() === now.toDateString(); + }) + .sort((a, b) => a.createdAt.seconds - b.createdAt.seconds); // Ordenar por fecha de creación + + console.log(announcements); + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +export async function addUserToPreregsiter(announcementId, user) { + try { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + if (!announcementSnapshot.exists()) { + throw new Error(`El anuncio con ID ${announcementId} no existe.`); + } + + const announcementData = announcementSnapshot.data(); + + const currentPreregs = announcementData.preregister || {}; + if (currentPreregs[user.uid]) { + throw new Error('Usuario ya registrado'); + } + const updatedPreregs = { + ...currentPreregs, + [user.uid]: user, + }; + + const currentAsistence = announcementData.asistence || {}; + const updatedAsistence = { + ...currentAsistence, + [user.uid]: false, + }; + + await updateDoc(announcementRef, { + preregister: updatedPreregs, + asistence: updatedAsistence, + }); + + console.log(`Usuario ${user.uid} agregado exitosamente a preregister y asistencia.`); + } catch (error) { + console.error('Error añadiendo usuario a preregister:', error); + throw error; + } +} + + +export async function processAsistence(announcementId) { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + const data = announcementSnapshot.data(); + + const preregister = data.preregister || {}; + const asistence = data.asistence || {}; + const dateTime = data.dateTime || ''; + + const preregisterKeys = Object.keys(preregister); + if (preregisterKeys.length === 0) { + console.log("No preregister data found."); + return []; + } + + const result = preregisterKeys.map(uid => { + const user = preregister[uid]; + + return { + uid: uid, + dateTime: dateTime, + name: user.name || '', + career: user.career || '', + area: user.area || '', + campus: user.campus || '', + asistence: asistence[uid] || false + }; + }); + + console.log("Result:", result); + return result; +} + + +export async function updateUserAsistence(announcementId, userId) { + try { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + + const announcementSnapshot = await getDoc(announcementRef); + if (!announcementSnapshot.exists()) { + throw new Error(`El anuncio con ID ${announcementId} no existe.`); + } + + const announcementData = announcementSnapshot.data(); + const currentAsistence = announcementData.asistence || {}; + const maesAsignados = announcementData.maesAsignados || []; + + const newAsistenceStatus = !currentAsistence[userId]; + const updatedAsistence = { + ...currentAsistence, + [userId]: newAsistenceStatus, + }; + + const totalMaes = maesAsignados.length; + + if (totalMaes > 0) { + const pointsPerMae = 50 / totalMaes; + + + for (const mae of maesAsignados) { + if (newAsistenceStatus) { + + await updatePoints(mae.uid, pointsPerMae); + console.log(`Puntos distribuidos a ${mae.name}: +${pointsPerMae}`); + } else { + + await updatePoints(mae.uid, -pointsPerMae); + console.log(`Puntos distribuidos a ${mae.name}: -${pointsPerMae}`); + } + } + } else { + console.log('No hay MAEs asignados para asignar puntos.'); + } + + await updateDoc(announcementRef, { + asistence: updatedAsistence, + }); + + console.log(`Asistencia para el usuario ${userId} actualizada exitosamente a ${newAsistenceStatus}.`); + } catch (error) { + console.error('Error actualizando la asistencia del usuario:', error); + throw error; + } +} + + +export async function processConfirms(announcementId) { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + const data = announcementSnapshot.data(); + + const preregister = data.preregister || {}; + const asistence = data.asistence || {}; + const dateTime = data.dateTime || ''; + + const preregisterKeys = Object.keys(preregister); + if (preregisterKeys.length === 0) { + console.log("No preregister data found."); + return []; + } + + const result = preregisterKeys + .filter(uid => asistence[uid] === true) + .map(uid => { + const user = preregister[uid]; + console.log("Processing user:", user); + + return { + uid: uid, + dateTime: dateTime, + name: user.name || '', + career: user.career || '', + area: user.area || '', + campus: user.campus || '', + asistence: true + }; + }); + return result; +} + + +export async function addExtraVariables() { + try { + const usersRef = collection(firestoreDB, 'announcements'); + const querySnapshot = await getDocs(usersRef); + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + return updateDoc(userRef, { + maesAsignados: [] + }); + + }); + + await Promise.all(promises); + + console.log("Background have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding background to eligible users: ", error); + throw error; + } +} + +export async function getAnnouncementsAllGrupales() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const q = query( + announcementsCollection, + where('type', '==', 'Asesoría') + ); + + const querySnapshot = await getDocs(q); + + const now = new Date(); + const announcements = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data(), + dateTime: doc.data().dateTime.toDate(), + })); + + const futureAnnouncements = announcements + .filter(announcement => + announcement.dateTime > now || + announcement.dateTime.toDateString() === now.toDateString() + ) + .sort((a, b) => a.dateTime - b.dateTime); + + const pastAnnouncements = announcements + .filter(announcement => + announcement.dateTime < now && + announcement.dateTime.toDateString() !== now.toDateString() + ) + .sort((a, b) => a.dateTime - b.dateTime); + + + const sortedAnnouncements = [...futureAnnouncements, ...pastAnnouncements]; + + + return sortedAnnouncements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + +export async function deleteAnnouncementById(id) { + try { + const announcementDocRef = doc(firestoreDB, "announcements", id); + + await deleteDoc(announcementDocRef); + + console.log(`Announcement with ID ${id} deleted successfully.`); + } catch (error) { + console.error(`Error deleting announcement with ID ${id}:`, error); + throw error; + } +} + +export async function updateAnnouncement(announcementId, updatedData) { + try { + const docRef = doc(firestoreDB, 'announcements', announcementId); + + await updateDoc(docRef, { + ...updatedData, + }); + + return docRef.id; + } catch (error) { + console.error('Error al actualizar el anuncio:', error); + throw error; + } +} + +export const toggleVisibilityById = async (id) => { + try { + console.log(id) + const dialogDocRef = doc(firestoreDB, 'announcements', id); + + const docSnap = await getDoc(dialogDocRef); + + if (docSnap.exists()) { + const currentVisibility = docSnap.data().visible + + await updateDoc(dialogDocRef, { + visible: !currentVisibility + }); + + console.log(`Visibilidad del diálogo con ID ${id} actualizada correctamente`); + } else { + console.log("El documento no existe"); + } + } catch (error) { + console.error(`Error al actualizar la visibilidad del diálogo con ID ${id}:`, error); + throw error; + } +}; diff --git a/src/firebase/db/asesorias.cached.js b/src/firebase/db/asesorias.cached.js new file mode 100644 index 000000000..2d098dc6f --- /dev/null +++ b/src/firebase/db/asesorias.cached.js @@ -0,0 +1,203 @@ +import * as asesoriaDb from './asesorias.legacy'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +const SEMESTER_START = new Date('2024-08-05'); + +function normalizeDateKey(date) { + if (!date) { + return 'all'; + } + + if (date instanceof Date) { + return date.toISOString(); + } + + return String(date); +} + +function getCurrentSemesterRange() { + const now = new Date(); + const currentYear = now.getFullYear(); + + if (now.getMonth() < 6) { + return { + start: new Date(currentYear, 0, 1), + end: new Date(currentYear, 5, 30, 23, 59, 59, 999) + }; + } + + return { + start: new Date(currentYear, 6, 1), + end: new Date(currentYear, 11, 31, 23, 59, 59, 999) + }; +} + +async function invalidateAsesoriaCaches() { + await invalidateCacheTags([CACHE_TAGS.ASESORIAS]); +} + +function timestampToMs(value) { + if (!value) return null; + if (typeof value?.toMillis === 'function') return value.toMillis(); + if (typeof value?.toDate === 'function') return value.toDate().getTime(); + if (value instanceof Date) return value.getTime(); + if (typeof value === 'string') return new Date(value).getTime(); + return null; +} + +export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { + const result = await asesoriaDb.addAsesoria(maeInfo, userInfo, subject, comment, rating); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getAsesorias(startDate = null, endDate = null, options = {}) { + const startKey = normalizeDateKey(startDate); + const endKey = normalizeDateKey(endDate); + + return await withCache( + cacheKeys.asesoriasRange(startKey, endKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await asesoriaDb.getAsesorias(startDate, endDate) + ); +} + +export async function getAsesoriasCountForUserInCurrentSemester(userId, options = {}) { + const { start, end } = getCurrentSemesterRange(); + const semesterKey = `${start.getFullYear()}-${start.getMonth() < 6 ? '01' : '02'}`; + + return await withCache( + cacheKeys.asesoriasSemesterByPeer(userId, semesterKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => { + const asesorias = await getAsesorias(start, end, options); + return (asesorias ?? []).filter((doc) => { + const dateMs = timestampToMs(doc.date); + return ( + doc.peerInfo?.uid === userId && + doc.duplicate !== true && + dateMs !== null && + dateMs >= start.getTime() && + dateMs <= end.getTime() + ); + }).length; + } + ); +} + +export async function getAsesoriasByUid(uid, options = {}) { + const today = new Date(); + const asesorias = await getAsesorias(SEMESTER_START, today, options); + return (asesorias ?? []).filter((asesoria) => asesoria.peerInfo?.uid === uid); +} + +export async function updateAllExperienceAsesorias() { + const result = await asesoriaDb.updateAllExperienceAsesorias(); + await invalidateAsesoriaCaches(); + return result; +} + +export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { + const result = await asesoriaDb.updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getCommentsByUid(uid, options = {}) { + const asesorias = await getAsesoriasByUid(uid, options); + return (asesorias ?? []).filter((asesoria) => asesoria.comment?.trim()); +} + +export async function getAsesoriasByUidAndRating(uidUser, uidPeer = null, options = {}) { + return await withCache( + cacheKeys.asesoriasPendingRating(uidUser, uidPeer ?? 'all'), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await asesoriaDb.getAsesoriasByUidAndRating(uidUser, uidPeer) + ); +} + +export async function updateAsesoria(id, data) { + const result = await asesoriaDb.updateAsesoria(id, data); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getTotalAsesorias(startDate = null, endDate = null, options = {}) { + const asesorias = await getAsesorias(startDate, endDate, options); + return (asesorias ?? []).length; +} + +export async function getAsesoriasCountByUser(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const userAsesoriasSet = new Set((asesorias ?? []).map(doc => doc.userInfo?.uid).filter(Boolean)); + return userAsesoriasSet.size; +} + +export async function getAsesoriasCountByArea(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const areasCount = {}; + + (asesorias ?? []).forEach((asesoria) => { + const subjectArea = asesoria?.subject?.area; + const userUid = asesoria?.userInfo?.uid; + + if (!subjectArea || !userUid) { + return; + } + + if (!areasCount[subjectArea]) { + areasCount[subjectArea] = { + totalAsesorias: 0, + userUids: new Set() + }; + } + + areasCount[subjectArea].totalAsesorias++; + areasCount[subjectArea].userUids.add(userUid); + }); + + return Object.keys(areasCount).map(area => ({ + area, + totalAsesorias: areasCount[area].totalAsesorias, + totalUniqueUsers: areasCount[area].userUids.size + })); +} + +export async function getAsesoriasCountByCampus(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const campusCount = {}; + + (asesorias ?? []).forEach((asesoria) => { + const campus = asesoria?.userInfo?.campus; + if (campus) { + campusCount[campus] = (campusCount[campus] || 0) + 1; + } + }); + + return Object.keys(campusCount).map(campus => ({ + campus, + totalAsesorias: campusCount[campus] + })); +} + +export async function deleteOldAsesorias() { + const result = await asesoriaDb.deleteOldAsesorias(); + await invalidateAsesoriaCaches(); + return result; +} diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 076cf624f..84b1fff34 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -1,455 +1 @@ -import { firestoreDB } from "../../main"; -import { - addDoc, - collection, - query, - where, - getDocs, - Timestamp, - updateDoc, - doc, - deleteDoc, -} from 'firebase/firestore'; -import { - updatePoints, - updateUserAchievementBadge -} from './users'; - -// Registra la asesoría del mae -export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { - // Changing to use payload instead to debug it - const payload = { - peerInfo: { - uid: maeInfo.uid, - name: maeInfo.name, - career: maeInfo.career, - profilePictureUrl: maeInfo.photoURL || '', // Uses the photoURL pretty sure lol instead of profilePictureURL for some reason -_- - // Datos para el excel - area: maeInfo.area || '', - campus: maeInfo.campus || '' - }, - userInfo: { - uid: userInfo.uid, - name: userInfo.name, - career: userInfo.career, - profilePictureUrl: userInfo.photoURL || userInfo.profilePictureUrl || '', // Shouldn't matter because it's the student pero ps si se echan redesign at some point - // Datos para excel - area: userInfo.area || '', - campus: userInfo.campus || '', - // Added pq me interesa, could help in the future si queremos detectar cuanta de la gente son alumnos o si son maes entre ellos - role: userInfo.role - }, - rating, - comment, - subject, - date: Timestamp.now() - }; - - // Debug para ver q se anden guardando los datos correctos - //console.log("Saving asesoria:", payload); - - await addDoc(collection(firestoreDB, "asesorias"), payload); - - updateExperienceAsesorias(maeInfo.uid, userInfo.uid, subject.id, Timestamp.now()); - return; -} - -export async function getAsesoriasCountForUserInCurrentSemester(userId) { - try { - const now = new Date(); - const currentYear = now.getFullYear(); - let startOfSemester, endOfSemester; - - // Determine the current semester range - if (now.getMonth() < 6) { // January to June - startOfSemester = new Date(currentYear, 0, 1); // January 1st - endOfSemester = new Date(currentYear, 5, 30, 23, 59, 59, 999); // June 30th, end of day - } else { // July to December - startOfSemester = new Date(currentYear, 6, 1); // July 1st - endOfSemester = new Date(currentYear, 11, 31, 23, 59, 59, 999); // December 31st, end of day - } - - const startMs = startOfSemester.getTime(); - const endMs = endOfSemester.getTime(); - const requestsRef = collection(firestoreDB, "asesorias"); - - // Realiza la consulta solo por userId - const q = query(requestsRef, where("peerInfo.uid", "==", userId)); - const querySnapshot = await getDocs(q); - - const filteredCount = querySnapshot.docs.filter(doc => { - const data = doc.data(); - - // Normaliza date -> milisegundos - let ms; - const dt = data.date; - if (!dt) return false; - if (dt instanceof Timestamp) ms = dt.toMillis(); - else if (dt.toDate) ms = dt.toDate().getTime(); - else if (dt instanceof Date) ms = dt.getTime(); - else if (typeof dt === "string") ms = new Date(dt).getTime(); - else return false; - - const isDuplicate = data.duplicate === true; - - return ms >= startMs && ms <= endMs && !isDuplicate; - }).length; - - return filteredCount; - } catch (error) { - console.error("Error fetching request count: ", error); - return 0; - } -} - - -export async function getAsesorias(startDate = null, endDate = null) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - let q; - - if (startDate && endDate) { - // Ajustar endDate para incluir todo el último día - const endDateAdjusted = new Date(endDate); - endDateAdjusted.setHours(23, 59, 59, 999); - - const startTimestamp = Timestamp.fromDate(new Date(startDate)); - const endTimestamp = Timestamp.fromDate(endDateAdjusted); - - q = query( - asesoriasRef, - where("date", ">=", startTimestamp), - where("date", "<=", endTimestamp) - ); - } else { - q = query(asesoriasRef); - } - - const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - - // Ordena las asesorías por fecha de la más reciente a la más antigua - asesorias.sort((a, b) => { - const dateA = a.date?.seconds || 0; - const dateB = b.date?.seconds || 0; - return dateB - dateA; - }); - - return asesorias; - } catch (error) { - console.error("Error fetching asesorias: ", error); - return []; - } -} - -// Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getAsesoriasByUid(uid) { - try { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - - const asesoriasFiltradas = asesorias.filter(asesoria => asesoria.peerInfo?.uid === uid); - - return asesoriasFiltradas; - } catch (error) { - console.error("Error fetching asesorias by UID: ", error); - return []; - } -} - -export async function updateAllExperienceAsesorias() { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - const processed = new Set(); // Conjunto para evitar procesar la misma asesoría más de una vez - - for (const advisory of asesorias) { - const { peerInfo, userInfo, date, subject } = advisory; - const advisoryDate = date.toDate(); - - // Generar clave única para evitar reprocesar la misma asesoría - const key = `${peerInfo.uid}-${userInfo.uid}-${subject.id}-${advisoryDate.getTime()}`; - if (processed.has(key)) continue; // Si ya se procesó, omitimos esta asesoría - processed.add(key); // Marcar como procesada - - // Encontrar asesorías similares en un rango de 2 horas - const similarAdvisories = asesorias.filter(ad => { - const adDate = ad.date.toDate(); - return ( - ad.peerInfo.uid === peerInfo.uid && - ad.userInfo.uid === userInfo.uid && - ad.subject.id === subject.id && - Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000 // 2 horas en ms - ); - }); - - // Iteramos sobre las asesorías similares y actualizamos el campo 'duplicate' - for (let i = 0; i < similarAdvisories.length; i++) { - const ad = similarAdvisories[i]; - const isDuplicate = i > 0; // La primera no es duplicada, las demás sí - - try { - await updateAdvisoryDuplicateField(ad.id, isDuplicate); - console.log( - ad, - isDuplicate - ? "Marcada como duplicada" - : "Primera ocurrencia - No duplicada" - ); - } catch (error) { - console.error(`Error al actualizar la asesoría con ID: ${ad.id}`, error); - } - } - } -} - -// Función auxiliar para actualizar el campo 'duplicate' en una asesoría -async function updateAdvisoryDuplicateField(advisoryDate, isDuplicate) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - - const q = query(asesoriasRef, where("date", "==", advisoryDate)); - const querySnapshot = await getDocs(q); - - if (querySnapshot.empty) { - console.log(`No se encontró ninguna asesoría con la fecha: ${advisoryDate}`); - return; - } - - const docRef = querySnapshot.docs[0].ref; - - await updateDoc(docRef, { duplicate: isDuplicate }); - - console.log(`Asesoría actualizada correctamente con fecha: ${advisoryDate}`); - } catch (error) { - console.error(`Error actualizando la asesoría con fecha ${advisoryDate}:`, error); - throw error; - } -} - - -// Función para actualizar puntos basados en asesorías similares -export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { - try { - if (!(advisoryDate instanceof Date)) { - if (advisoryDate.toDate) { - advisoryDate = advisoryDate.toDate(); - } else { - advisoryDate = new Date(advisoryDate); - } - } - - const today = Timestamp.now().toDate(); - const startOfDay = new Date(today); - startOfDay.setHours(0, 0, 0, 0); - const endOfDay = new Date(today); - endOfDay.setHours(23, 59, 59, 999); - - const asesoriasRef = collection(firestoreDB, "asesorias"); - - const q = query( - asesoriasRef, - where("date", ">=", startOfDay), - where("date", "<=", endOfDay) - ); - - - const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => doc.data()); - const similarAdvisories = asesorias.filter(ad => { - const adDate = ad.date.toDate(); - return ad.peerInfo.uid === peerUid && - ad.userInfo.uid === userUid && - ad.subject.id === subjectId && - Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000; - }); - const ultimoElemento = asesorias[asesorias.length - 1]; - if (similarAdvisories.length > 1) { - await updatePoints(peerUid, -150); - await updateAdvisoryDuplicateField(ultimoElemento.date, true ); - await updateUserAchievementBadge(userUid, "18"); - } else { - await updateAdvisoryDuplicateField(ultimoElemento.date, false ); - if(subjectId === "MAE"){ - await updatePoints(peerUid, 15); - }else{ - await updatePoints(peerUid, 60); - } - - } - - } catch (error) { - console.error("Error actualizando la experiencia de asesorías:", error); - } -} - -// Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getCommentsByUid(uid) { - try { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - - const asesoriasFiltradas = asesorias.filter(asesoria => - asesoria.peerInfo?.uid === uid && asesoria.comment?.trim() - ); - - return asesoriasFiltradas; - } catch (error) { - console.error("Error fetching asesorias by UID: ", error); - return []; - } -} - - -export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - - let queryConstraints = [ - where("userInfo.uid", "==", uidUser), - where("rating", "==", null), - where("duplicate", "==", false), - ]; - - if (uidPeer) { - queryConstraints.unshift( where("peerInfo.uid", "==", uidPeer)); - } - - const q = query(asesoriasRef, ...queryConstraints); - const querySnapshot = await getDocs(q); - - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - - return asesorias; - } catch (error) { - console.error("Error fetching asesorias: ", error); - return []; - } -} -export async function updateAsesoria(id, data) { - try { - - const asesoriaRef = doc(firestoreDB, "asesorias", id); - await updateDoc(asesoriaRef, data); - - console.log("Asesoria actualizada exitosamente"); - } catch (error) { - console.error("Error updating asesoria: ", error); - } - } - - - export async function getTotalAsesorias(startDate = null, endDate = null) { - try { - const asesorias = await getAsesorias(startDate, endDate); - const totalAsesorias = asesorias.length; - return totalAsesorias; - } catch (error) { - console.error("Error fetching total asesorias: ", error); - return 0; - } -} - - -export async function getAsesoriasCountByUser() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const userAsesoriasSet = new Set( - querySnapshot.docs.map(doc => doc.data().userInfo?.uid).filter(Boolean) - ); - return userAsesoriasSet.size; - } catch (error) { - console.error("Error al obtener el conteo de asesorías por usuario: ", error); - throw error; - } -} - -export async function getAsesoriasCountByArea() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const areasCount = {}; - - querySnapshot.docs.forEach(doc => { - const asesoríaData = doc.data(); - const subjectArea = asesoríaData?.subject?.area; - const userUid = asesoríaData?.userInfo?.uid; - - if (subjectArea && userUid) { - if (!areasCount[subjectArea]) { - areasCount[subjectArea] = { - totalAsesorias: 0, - userUids: new Set() - }; - } - - areasCount[subjectArea].totalAsesorias++; - areasCount[subjectArea].userUids.add(userUid); - } - }); - - return Object.keys(areasCount).map(area => ({ - area, - totalAsesorias: areasCount[area].totalAsesorias, - totalUniqueUsers: areasCount[area].userUids.size - })); - } catch (error) { - console.error("Error al obtener el conteo de asesorías y usuarios por área: ", error); - throw error; - } -} - - -export async function getAsesoriasCountByCampus() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const campusCount = {}; - - querySnapshot.forEach(doc => { - const campus = doc.data()?.userInfo?.campus; - if (campus) { - campusCount[campus] = (campusCount[campus] || 0) + 1; - } - }); - - return Object.keys(campusCount).map(campus => ({ - campus, - totalAsesorias: campusCount[campus] - })); - } catch (error) { - console.error("Error al obtener el conteo de asesorías por campus: ", error); - throw error; - } -} - -// Eliminar todas las asesorias pasadas -export async function deleteOldAsesorias() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const currentYear = new Date().getFullYear(); - - const deletePromises = []; - - querySnapshot.forEach(docSnapshot => { - const asesoriasData = docSnapshot.data(); - const asesoriasDate = asesoriasData?.date ? new Date(asesoriasData.date) : null; - - if (asesoriasDate && asesoriasDate.getFullYear() !== currentYear) { - deletePromises.push(deleteDoc(doc(firestoreDB, "asesorias", docSnapshot.id))); - } - }); - - await Promise.all(deletePromises); - console.log("Asesorías antiguas eliminadas correctamente."); - } catch (error) { - console.error("Error al eliminar asesorías antiguas: ", error); - throw error; - } -} \ No newline at end of file +export * from './asesorias.cached'; diff --git a/src/firebase/db/asesorias.legacy.js b/src/firebase/db/asesorias.legacy.js new file mode 100644 index 000000000..076cf624f --- /dev/null +++ b/src/firebase/db/asesorias.legacy.js @@ -0,0 +1,455 @@ +import { firestoreDB } from "../../main"; +import { + addDoc, + collection, + query, + where, + getDocs, + Timestamp, + updateDoc, + doc, + deleteDoc, +} from 'firebase/firestore'; +import { + updatePoints, + updateUserAchievementBadge +} from './users'; + +// Registra la asesoría del mae +export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { + // Changing to use payload instead to debug it + const payload = { + peerInfo: { + uid: maeInfo.uid, + name: maeInfo.name, + career: maeInfo.career, + profilePictureUrl: maeInfo.photoURL || '', // Uses the photoURL pretty sure lol instead of profilePictureURL for some reason -_- + // Datos para el excel + area: maeInfo.area || '', + campus: maeInfo.campus || '' + }, + userInfo: { + uid: userInfo.uid, + name: userInfo.name, + career: userInfo.career, + profilePictureUrl: userInfo.photoURL || userInfo.profilePictureUrl || '', // Shouldn't matter because it's the student pero ps si se echan redesign at some point + // Datos para excel + area: userInfo.area || '', + campus: userInfo.campus || '', + // Added pq me interesa, could help in the future si queremos detectar cuanta de la gente son alumnos o si son maes entre ellos + role: userInfo.role + }, + rating, + comment, + subject, + date: Timestamp.now() + }; + + // Debug para ver q se anden guardando los datos correctos + //console.log("Saving asesoria:", payload); + + await addDoc(collection(firestoreDB, "asesorias"), payload); + + updateExperienceAsesorias(maeInfo.uid, userInfo.uid, subject.id, Timestamp.now()); + return; +} + +export async function getAsesoriasCountForUserInCurrentSemester(userId) { + try { + const now = new Date(); + const currentYear = now.getFullYear(); + let startOfSemester, endOfSemester; + + // Determine the current semester range + if (now.getMonth() < 6) { // January to June + startOfSemester = new Date(currentYear, 0, 1); // January 1st + endOfSemester = new Date(currentYear, 5, 30, 23, 59, 59, 999); // June 30th, end of day + } else { // July to December + startOfSemester = new Date(currentYear, 6, 1); // July 1st + endOfSemester = new Date(currentYear, 11, 31, 23, 59, 59, 999); // December 31st, end of day + } + + const startMs = startOfSemester.getTime(); + const endMs = endOfSemester.getTime(); + const requestsRef = collection(firestoreDB, "asesorias"); + + // Realiza la consulta solo por userId + const q = query(requestsRef, where("peerInfo.uid", "==", userId)); + const querySnapshot = await getDocs(q); + + const filteredCount = querySnapshot.docs.filter(doc => { + const data = doc.data(); + + // Normaliza date -> milisegundos + let ms; + const dt = data.date; + if (!dt) return false; + if (dt instanceof Timestamp) ms = dt.toMillis(); + else if (dt.toDate) ms = dt.toDate().getTime(); + else if (dt instanceof Date) ms = dt.getTime(); + else if (typeof dt === "string") ms = new Date(dt).getTime(); + else return false; + + const isDuplicate = data.duplicate === true; + + return ms >= startMs && ms <= endMs && !isDuplicate; + }).length; + + return filteredCount; + } catch (error) { + console.error("Error fetching request count: ", error); + return 0; + } +} + + +export async function getAsesorias(startDate = null, endDate = null) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); + let q; + + if (startDate && endDate) { + // Ajustar endDate para incluir todo el último día + const endDateAdjusted = new Date(endDate); + endDateAdjusted.setHours(23, 59, 59, 999); + + const startTimestamp = Timestamp.fromDate(new Date(startDate)); + const endTimestamp = Timestamp.fromDate(endDateAdjusted); + + q = query( + asesoriasRef, + where("date", ">=", startTimestamp), + where("date", "<=", endTimestamp) + ); + } else { + q = query(asesoriasRef); + } + + const querySnapshot = await getDocs(q); + const asesorias = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + + // Ordena las asesorías por fecha de la más reciente a la más antigua + asesorias.sort((a, b) => { + const dateA = a.date?.seconds || 0; + const dateB = b.date?.seconds || 0; + return dateB - dateA; + }); + + return asesorias; + } catch (error) { + console.error("Error fetching asesorias: ", error); + return []; + } +} + +// Función para obtener asesorías por UID, reutilizando getAsesorias +export async function getAsesoriasByUid(uid) { + try { + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + + const asesoriasFiltradas = asesorias.filter(asesoria => asesoria.peerInfo?.uid === uid); + + return asesoriasFiltradas; + } catch (error) { + console.error("Error fetching asesorias by UID: ", error); + return []; + } +} + +export async function updateAllExperienceAsesorias() { + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + const processed = new Set(); // Conjunto para evitar procesar la misma asesoría más de una vez + + for (const advisory of asesorias) { + const { peerInfo, userInfo, date, subject } = advisory; + const advisoryDate = date.toDate(); + + // Generar clave única para evitar reprocesar la misma asesoría + const key = `${peerInfo.uid}-${userInfo.uid}-${subject.id}-${advisoryDate.getTime()}`; + if (processed.has(key)) continue; // Si ya se procesó, omitimos esta asesoría + processed.add(key); // Marcar como procesada + + // Encontrar asesorías similares en un rango de 2 horas + const similarAdvisories = asesorias.filter(ad => { + const adDate = ad.date.toDate(); + return ( + ad.peerInfo.uid === peerInfo.uid && + ad.userInfo.uid === userInfo.uid && + ad.subject.id === subject.id && + Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000 // 2 horas en ms + ); + }); + + // Iteramos sobre las asesorías similares y actualizamos el campo 'duplicate' + for (let i = 0; i < similarAdvisories.length; i++) { + const ad = similarAdvisories[i]; + const isDuplicate = i > 0; // La primera no es duplicada, las demás sí + + try { + await updateAdvisoryDuplicateField(ad.id, isDuplicate); + console.log( + ad, + isDuplicate + ? "Marcada como duplicada" + : "Primera ocurrencia - No duplicada" + ); + } catch (error) { + console.error(`Error al actualizar la asesoría con ID: ${ad.id}`, error); + } + } + } +} + +// Función auxiliar para actualizar el campo 'duplicate' en una asesoría +async function updateAdvisoryDuplicateField(advisoryDate, isDuplicate) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); + + const q = query(asesoriasRef, where("date", "==", advisoryDate)); + const querySnapshot = await getDocs(q); + + if (querySnapshot.empty) { + console.log(`No se encontró ninguna asesoría con la fecha: ${advisoryDate}`); + return; + } + + const docRef = querySnapshot.docs[0].ref; + + await updateDoc(docRef, { duplicate: isDuplicate }); + + console.log(`Asesoría actualizada correctamente con fecha: ${advisoryDate}`); + } catch (error) { + console.error(`Error actualizando la asesoría con fecha ${advisoryDate}:`, error); + throw error; + } +} + + +// Función para actualizar puntos basados en asesorías similares +export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { + try { + if (!(advisoryDate instanceof Date)) { + if (advisoryDate.toDate) { + advisoryDate = advisoryDate.toDate(); + } else { + advisoryDate = new Date(advisoryDate); + } + } + + const today = Timestamp.now().toDate(); + const startOfDay = new Date(today); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(today); + endOfDay.setHours(23, 59, 59, 999); + + const asesoriasRef = collection(firestoreDB, "asesorias"); + + const q = query( + asesoriasRef, + where("date", ">=", startOfDay), + where("date", "<=", endOfDay) + ); + + + const querySnapshot = await getDocs(q); + const asesorias = querySnapshot.docs.map(doc => doc.data()); + const similarAdvisories = asesorias.filter(ad => { + const adDate = ad.date.toDate(); + return ad.peerInfo.uid === peerUid && + ad.userInfo.uid === userUid && + ad.subject.id === subjectId && + Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000; + }); + const ultimoElemento = asesorias[asesorias.length - 1]; + if (similarAdvisories.length > 1) { + await updatePoints(peerUid, -150); + await updateAdvisoryDuplicateField(ultimoElemento.date, true ); + await updateUserAchievementBadge(userUid, "18"); + } else { + await updateAdvisoryDuplicateField(ultimoElemento.date, false ); + if(subjectId === "MAE"){ + await updatePoints(peerUid, 15); + }else{ + await updatePoints(peerUid, 60); + } + + } + + } catch (error) { + console.error("Error actualizando la experiencia de asesorías:", error); + } +} + +// Función para obtener asesorías por UID, reutilizando getAsesorias +export async function getCommentsByUid(uid) { + try { + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + + const asesoriasFiltradas = asesorias.filter(asesoria => + asesoria.peerInfo?.uid === uid && asesoria.comment?.trim() + ); + + return asesoriasFiltradas; + } catch (error) { + console.error("Error fetching asesorias by UID: ", error); + return []; + } +} + + +export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); + + let queryConstraints = [ + where("userInfo.uid", "==", uidUser), + where("rating", "==", null), + where("duplicate", "==", false), + ]; + + if (uidPeer) { + queryConstraints.unshift( where("peerInfo.uid", "==", uidPeer)); + } + + const q = query(asesoriasRef, ...queryConstraints); + const querySnapshot = await getDocs(q); + + const asesorias = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + + return asesorias; + } catch (error) { + console.error("Error fetching asesorias: ", error); + return []; + } +} +export async function updateAsesoria(id, data) { + try { + + const asesoriaRef = doc(firestoreDB, "asesorias", id); + await updateDoc(asesoriaRef, data); + + console.log("Asesoria actualizada exitosamente"); + } catch (error) { + console.error("Error updating asesoria: ", error); + } + } + + + export async function getTotalAsesorias(startDate = null, endDate = null) { + try { + const asesorias = await getAsesorias(startDate, endDate); + const totalAsesorias = asesorias.length; + return totalAsesorias; + } catch (error) { + console.error("Error fetching total asesorias: ", error); + return 0; + } +} + + +export async function getAsesoriasCountByUser() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const userAsesoriasSet = new Set( + querySnapshot.docs.map(doc => doc.data().userInfo?.uid).filter(Boolean) + ); + return userAsesoriasSet.size; + } catch (error) { + console.error("Error al obtener el conteo de asesorías por usuario: ", error); + throw error; + } +} + +export async function getAsesoriasCountByArea() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const areasCount = {}; + + querySnapshot.docs.forEach(doc => { + const asesoríaData = doc.data(); + const subjectArea = asesoríaData?.subject?.area; + const userUid = asesoríaData?.userInfo?.uid; + + if (subjectArea && userUid) { + if (!areasCount[subjectArea]) { + areasCount[subjectArea] = { + totalAsesorias: 0, + userUids: new Set() + }; + } + + areasCount[subjectArea].totalAsesorias++; + areasCount[subjectArea].userUids.add(userUid); + } + }); + + return Object.keys(areasCount).map(area => ({ + area, + totalAsesorias: areasCount[area].totalAsesorias, + totalUniqueUsers: areasCount[area].userUids.size + })); + } catch (error) { + console.error("Error al obtener el conteo de asesorías y usuarios por área: ", error); + throw error; + } +} + + +export async function getAsesoriasCountByCampus() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const campusCount = {}; + + querySnapshot.forEach(doc => { + const campus = doc.data()?.userInfo?.campus; + if (campus) { + campusCount[campus] = (campusCount[campus] || 0) + 1; + } + }); + + return Object.keys(campusCount).map(campus => ({ + campus, + totalAsesorias: campusCount[campus] + })); + } catch (error) { + console.error("Error al obtener el conteo de asesorías por campus: ", error); + throw error; + } +} + +// Eliminar todas las asesorias pasadas +export async function deleteOldAsesorias() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const currentYear = new Date().getFullYear(); + + const deletePromises = []; + + querySnapshot.forEach(docSnapshot => { + const asesoriasData = docSnapshot.data(); + const asesoriasDate = asesoriasData?.date ? new Date(asesoriasData.date) : null; + + if (asesoriasDate && asesoriasDate.getFullYear() !== currentYear) { + deletePromises.push(deleteDoc(doc(firestoreDB, "asesorias", docSnapshot.id))); + } + }); + + await Promise.all(deletePromises); + console.log("Asesorías antiguas eliminadas correctamente."); + } catch (error) { + console.error("Error al eliminar asesorías antiguas: ", error); + throw error; + } +} \ No newline at end of file diff --git a/src/firebase/db/attendance.cached.js b/src/firebase/db/attendance.cached.js new file mode 100644 index 000000000..7dc11e5b6 --- /dev/null +++ b/src/firebase/db/attendance.cached.js @@ -0,0 +1,99 @@ +import * as attendanceDb from './attendance.legacy'; +import { attendanceDateTag, CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; +import { invalidateCacheTags, withCache } from '../cache/cache'; + +function getCurrentDateFormatted() { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function resolveAttendanceTtl(dateString) { + return dateString === getCurrentDateFormatted() ? CACHE_TTL_MS.ATTENDANCE_TODAY : CACHE_TTL_MS.ATTENDANCE_DAY; +} + +async function invalidateAttendanceForDate(dateString) { + await invalidateCacheTags([CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)]); +} + +export async function getTodaysReport(options = {}) { + const today = getCurrentDateFormatted(); + + return await withCache( + cacheKeys.attendanceToday(today), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] + }, + async () => await attendanceDb.getTodaysReport() + ); +} + +export async function updateReport(userInfo, report) { + const result = await attendanceDb.updateReport(userInfo, report); + await invalidateAttendanceForDate(getCurrentDateFormatted()); + return result; +} + +export async function updateReportByDate(userInfo, date, report) { + const result = await attendanceDb.updateReportByDate(userInfo, date, report); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + await invalidateAttendanceForDate(`${year}-${month}-${day}`); + return result; +} + +export async function addRegister(userInfo, date) { + const result = await attendanceDb.addRegister(userInfo, date); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + await invalidateAttendanceForDate(`${year}-${month}-${day}`); + return result; +} + +export async function getStudentReport(uid, options = {}) { + const today = getCurrentDateFormatted(); + + return await withCache( + cacheKeys.attendanceStudent(uid, today), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] + }, + async () => await attendanceDb.getStudentReport(uid) + ); +} + +export async function getReportByDate(dateString, options = {}) { + return await withCache( + cacheKeys.attendanceByDate(dateString), + { + ttlMs: resolveAttendanceTtl(dateString), + persist: dateString !== getCurrentDateFormatted(), + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)] + }, + async () => await attendanceDb.getReportByDate(dateString) + ); +} + +export async function getReportByDateRange(startDate, endDate, options = {}) { + return await withCache( + cacheKeys.attendanceRange(startDate, endDate), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_RANGE, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE] + }, + async () => await attendanceDb.getReportByDateRange(startDate, endDate) + ); +} diff --git a/src/firebase/db/attendance.js b/src/firebase/db/attendance.js index ab6b83aba..4505784b8 100644 --- a/src/firebase/db/attendance.js +++ b/src/firebase/db/attendance.js @@ -1,219 +1 @@ -import { firestoreDB } from "../../main"; -import { - doc, - getDoc, - getDocs, - setDoc, - collection, -} from 'firebase/firestore'; - -function getCurrentDateFormatted() { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based - const day = String(today.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; -} - -export async function getTodaysReport() { - try { - const reportRef = collection(firestoreDB, "attendance", getCurrentDateFormatted(), "report"); - // const reportRef = collection(firestoreDB, "attendance", "2024-05-16", "report"); - const reportSnapshot = await getDocs(reportRef); - - let report = {} - - reportSnapshot.forEach((doc) => { - const docData = doc.data(); - report[doc.id] = docData.report; - }); - - return report; - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -// Update the MAE attendance report w corresponding value -export async function updateReport(userInfo, report) { - try { - // Defensive checks + unwrap reactive proxy - const uid = userInfo?.uid ?? userInfo?.id ?? userInfo?.value?.uid; - const name = userInfo?.name ?? userInfo?.value?.name ?? ''; - const totalTime = userInfo?.totalTime ?? userInfo?.value?.totalTime ?? 0; - - console.log(uid, report, "Updating report") - const reportRef = doc(firestoreDB, "attendance", getCurrentDateFormatted(), "report", userInfo.uid); // Final de semestre, quitar report de aca y luego when accessing data para que sean menos datos - - // Stores less data for attendance - const dataUpload = { - id: userInfo.uid, // Student id - email: userInfo.email, // Student email, helps search data within firebase - name: userInfo.name, - totalTime: userInfo.totalTime, - report: report, // (A, R, F, J) - } - - console.log('Writing to Firestore path:', reportRef.path, 'payload:', dataUpload); - - return await setDoc(reportRef, dataUpload, { merge : true }); // Use merge so that it can keep otehr fields if write more data - } catch (error) { - console.error("Error updating the report: ", error); - return []; - } -} - -// Update attendance report for a specific date (used for makeup attendance) -export async function updateReportByDate(userInfo, date, report) { - try { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const dateString = `${year}-${month}-${day}`; - - const dateDocRef = doc(firestoreDB, "attendance", dateString); - await setDoc(dateDocRef, { initialized: true }, { merge: true }); - - const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); - await setDoc(reportRef, { - id: userInfo.uid, - email: userInfo.email, - name: userInfo.name, - totalTime: userInfo.totalTime, - report: report, - }, { merge: true }); - } catch (error) { - console.error("Error updating report by date: ", error); - } -} - -// To get date info -export async function addRegister(userInfo, date) { - try { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const dateString = `${year}-${month}-${day}`; - - // Root date doc is created w dummy field - const dateDocRef = doc(firestoreDB, "attendance", dateString); - await setDoc(dateDocRef, { initialized: true }, { merge: true }); - - const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); - await setDoc(reportRef, { - ...userInfo, - report: 'RR' - }); - - } catch (error) { - console.error("Error updating the report: ", error); - } -} - -export async function getStudentReport(uid) { - const d = new Date(); - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const date = `${y}-${m}-${day}`; - const reportRef = doc(firestoreDB, "attendance", date, "report", uid); - const snap = await getDoc(reportRef); - - if (snap.exists()) { - return snap.data().report; // 'A', 'J', 'R', 'F' - } else { - return null; - } -} - - -// Para obtener los datos de asistencia de una fecha -export async function getReportByDate (dateString) { - try { - // Reference with root de attendance, document es dateString del input parameter, y luego report subcollection - const reportRef = collection(firestoreDB, "attendance", dateString, "report"); - const reportSnapshot = await getDocs(reportRef); // Arreglo del reporte - - let report = {}; // Objeto vacío para llenarlo de datos - - // For each reportSnapshot in array, extracts the data using document id as key (so matricula) - reportSnapshot.forEach((doc) => { - const docData = doc.data(); - report[doc.id] = docData; // or docData.report if needed - }); - - return report; - // Error debug - } catch (error) { - console.error("Error fetching report: ", error); - return {}; - } -} - -// Helper funct, gets dates between specified start and end date -function getDateStringsBetween(startDate, endDate) { - // Handle start and end differently because of time zones, shift to make them back because default at GMT-0600 so 6 hrs ahead -_- - const start = new Date(startDate + 'T12:00:00'); // Set to noon instead - const end = new Date(endDate + 'T12:00:00'); - const dateList = []; - - const currDate = new Date(start); // Sets start as current - - // Fetch all days in between the range - while (currDate <= end) { - const year = currDate.getFullYear(); - const month = String(currDate.getMonth() + 1).padStart(2, '0'); // Gets month, adds 0 if just one digit - const day = String(currDate.getDate()).padStart(2, '0'); // Gets date and adds 0 if just one digit - - //console.log(` Current date: ${currDate}, End date: ${end}`); - //console.log(` Comparison result: ${currDate <= end}`); - - // Save and upgrade for next iteration - dateList.push(`${year}-${month}-${day}`); // Adds formatted date to list for firebase use - currDate.setDate(currDate.getDate() + 1); // Moves to check next date - - //console.log(` After increment: ${currDate}`); - - - } - return dateList; -} - -// Gets the attendance reports for every day -export async function getReportByDateRange(startDate, endDate) { - const dateStrings = getDateStringsBetween(startDate, endDate); - const report = []; - - // Checks each document date w the reports - for (const date of dateStrings) { - const reportRef = collection(firestoreDB, "attendance", date, "report"); - try { - const reportSnap = await getDocs(reportRef); - // Makes sure not empty date w no attendance - if (!reportSnap.empty) { - //console.log(`Found ${reportSnap.size} reports for ${date}`); - reportSnap.forEach((doc) => { - /*report.push({ - id: doc.id, - ...doc.data(), - date, - });*/ - const data = doc.data(); - // Only keeps id and report, modify if want other fields (like name or email) - report.push({ - id: doc.id, // Student matricula - report: data.report, // (A, R, F, J) - }); - }); - } else { - console.log(`No reports ${date}`); - } - } catch (error) { - console.warn(`Skipping ${date}:`, error.message); - } - } - - return report; -} \ No newline at end of file +export * from './attendance.cached'; diff --git a/src/firebase/db/attendance.legacy.js b/src/firebase/db/attendance.legacy.js new file mode 100644 index 000000000..ab6b83aba --- /dev/null +++ b/src/firebase/db/attendance.legacy.js @@ -0,0 +1,219 @@ +import { firestoreDB } from "../../main"; +import { + doc, + getDoc, + getDocs, + setDoc, + collection, +} from 'firebase/firestore'; + +function getCurrentDateFormatted() { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based + const day = String(today.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +export async function getTodaysReport() { + try { + const reportRef = collection(firestoreDB, "attendance", getCurrentDateFormatted(), "report"); + // const reportRef = collection(firestoreDB, "attendance", "2024-05-16", "report"); + const reportSnapshot = await getDocs(reportRef); + + let report = {} + + reportSnapshot.forEach((doc) => { + const docData = doc.data(); + report[doc.id] = docData.report; + }); + + return report; + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +// Update the MAE attendance report w corresponding value +export async function updateReport(userInfo, report) { + try { + // Defensive checks + unwrap reactive proxy + const uid = userInfo?.uid ?? userInfo?.id ?? userInfo?.value?.uid; + const name = userInfo?.name ?? userInfo?.value?.name ?? ''; + const totalTime = userInfo?.totalTime ?? userInfo?.value?.totalTime ?? 0; + + console.log(uid, report, "Updating report") + const reportRef = doc(firestoreDB, "attendance", getCurrentDateFormatted(), "report", userInfo.uid); // Final de semestre, quitar report de aca y luego when accessing data para que sean menos datos + + // Stores less data for attendance + const dataUpload = { + id: userInfo.uid, // Student id + email: userInfo.email, // Student email, helps search data within firebase + name: userInfo.name, + totalTime: userInfo.totalTime, + report: report, // (A, R, F, J) + } + + console.log('Writing to Firestore path:', reportRef.path, 'payload:', dataUpload); + + return await setDoc(reportRef, dataUpload, { merge : true }); // Use merge so that it can keep otehr fields if write more data + } catch (error) { + console.error("Error updating the report: ", error); + return []; + } +} + +// Update attendance report for a specific date (used for makeup attendance) +export async function updateReportByDate(userInfo, date, report) { + try { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateString = `${year}-${month}-${day}`; + + const dateDocRef = doc(firestoreDB, "attendance", dateString); + await setDoc(dateDocRef, { initialized: true }, { merge: true }); + + const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); + await setDoc(reportRef, { + id: userInfo.uid, + email: userInfo.email, + name: userInfo.name, + totalTime: userInfo.totalTime, + report: report, + }, { merge: true }); + } catch (error) { + console.error("Error updating report by date: ", error); + } +} + +// To get date info +export async function addRegister(userInfo, date) { + try { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateString = `${year}-${month}-${day}`; + + // Root date doc is created w dummy field + const dateDocRef = doc(firestoreDB, "attendance", dateString); + await setDoc(dateDocRef, { initialized: true }, { merge: true }); + + const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); + await setDoc(reportRef, { + ...userInfo, + report: 'RR' + }); + + } catch (error) { + console.error("Error updating the report: ", error); + } +} + +export async function getStudentReport(uid) { + const d = new Date(); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const date = `${y}-${m}-${day}`; + const reportRef = doc(firestoreDB, "attendance", date, "report", uid); + const snap = await getDoc(reportRef); + + if (snap.exists()) { + return snap.data().report; // 'A', 'J', 'R', 'F' + } else { + return null; + } +} + + +// Para obtener los datos de asistencia de una fecha +export async function getReportByDate (dateString) { + try { + // Reference with root de attendance, document es dateString del input parameter, y luego report subcollection + const reportRef = collection(firestoreDB, "attendance", dateString, "report"); + const reportSnapshot = await getDocs(reportRef); // Arreglo del reporte + + let report = {}; // Objeto vacío para llenarlo de datos + + // For each reportSnapshot in array, extracts the data using document id as key (so matricula) + reportSnapshot.forEach((doc) => { + const docData = doc.data(); + report[doc.id] = docData; // or docData.report if needed + }); + + return report; + // Error debug + } catch (error) { + console.error("Error fetching report: ", error); + return {}; + } +} + +// Helper funct, gets dates between specified start and end date +function getDateStringsBetween(startDate, endDate) { + // Handle start and end differently because of time zones, shift to make them back because default at GMT-0600 so 6 hrs ahead -_- + const start = new Date(startDate + 'T12:00:00'); // Set to noon instead + const end = new Date(endDate + 'T12:00:00'); + const dateList = []; + + const currDate = new Date(start); // Sets start as current + + // Fetch all days in between the range + while (currDate <= end) { + const year = currDate.getFullYear(); + const month = String(currDate.getMonth() + 1).padStart(2, '0'); // Gets month, adds 0 if just one digit + const day = String(currDate.getDate()).padStart(2, '0'); // Gets date and adds 0 if just one digit + + //console.log(` Current date: ${currDate}, End date: ${end}`); + //console.log(` Comparison result: ${currDate <= end}`); + + // Save and upgrade for next iteration + dateList.push(`${year}-${month}-${day}`); // Adds formatted date to list for firebase use + currDate.setDate(currDate.getDate() + 1); // Moves to check next date + + //console.log(` After increment: ${currDate}`); + + + } + return dateList; +} + +// Gets the attendance reports for every day +export async function getReportByDateRange(startDate, endDate) { + const dateStrings = getDateStringsBetween(startDate, endDate); + const report = []; + + // Checks each document date w the reports + for (const date of dateStrings) { + const reportRef = collection(firestoreDB, "attendance", date, "report"); + try { + const reportSnap = await getDocs(reportRef); + // Makes sure not empty date w no attendance + if (!reportSnap.empty) { + //console.log(`Found ${reportSnap.size} reports for ${date}`); + reportSnap.forEach((doc) => { + /*report.push({ + id: doc.id, + ...doc.data(), + date, + });*/ + const data = doc.data(); + // Only keeps id and report, modify if want other fields (like name or email) + report.push({ + id: doc.id, // Student matricula + report: data.report, // (A, R, F, J) + }); + }); + } else { + console.log(`No reports ${date}`); + } + } catch (error) { + console.warn(`Skipping ${date}:`, error.message); + } + } + + return report; +} \ No newline at end of file diff --git a/src/firebase/db/campuses.cached.js b/src/firebase/db/campuses.cached.js new file mode 100644 index 000000000..df9a2b948 --- /dev/null +++ b/src/firebase/db/campuses.cached.js @@ -0,0 +1,29 @@ +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, +} from 'firebase/firestore'; +import { withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getCampuses(options = {}) { + return await withCache( + cacheKeys.campuses(), + { + ttlMs: CACHE_TTL_MS.CAMPUSES, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.CAMPUSES] + }, + async () => { + const campusesRef = collection(firestoreDB, "schools/tec.mx/campus"); + const docsSnap = await getDocs(campusesRef); + + if (docsSnap) { + return docsSnap.docs.map(doc => doc.data()); + } + + return null; + } + ); +} diff --git a/src/firebase/db/campuses.js b/src/firebase/db/campuses.js index 7e32bbcf6..ae8dc3736 100644 --- a/src/firebase/db/campuses.js +++ b/src/firebase/db/campuses.js @@ -1,17 +1 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, -} from 'firebase/firestore'; - -export async function getCampuses() { - const campusesRef = collection(firestoreDB, "schools/tec.mx/campus"); - - const docsSnap = await getDocs(campusesRef); - - if (docsSnap) { - return docsSnap.docs.map(doc => doc.data()); - } else { - return null; - } -} \ No newline at end of file +export * from './campuses.cached'; diff --git a/src/firebase/db/maeteca.cached.js b/src/firebase/db/maeteca.cached.js new file mode 100644 index 000000000..709b38b21 --- /dev/null +++ b/src/firebase/db/maeteca.cached.js @@ -0,0 +1,419 @@ +// Filtra videos por texto en título o descripción, normalizado (MaesActivos style) +import { normalize } from '@/utils/HorarioUtils'; +export function filterVideosByText(videos, text) { + if (!Array.isArray(videos)) return []; + const normalizeText = typeof text === 'string' ? text : ''; + const query = normalize(normalizeText || ''); + if (!query) return videos; + return videos.filter(video => { + const title = normalize(video.Titulo || ''); + const info = normalize(video.Informacion || ''); + return title.includes(query) || info.includes(query); + }); +} +import { firestoreDB } from "../../main"; +import { + getDocs, + addDoc, + collection, + serverTimestamp +} from 'firebase/firestore'; +import { getCurrentUser } from './users'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export const VIDEO_MANAGER_ROLES = ['admin', 'tec', 'coordi']; + +function assertVideoPermissions(user) { + if (!VIDEO_MANAGER_ROLES.includes(user?.role)) { + const role = user?.role ?? 'unknown'; + throw new Error(`Insufficient permissions for role '${role}' to manage Maeteca videos`); + } +} + +async function invalidateVideoCaches() { + await invalidateCacheTags([CACHE_TAGS.VIDEOS]); +} + +async function fetchAllVideosFresh() { + const videosRef = collection(firestoreDB, "videos"); + const snapshot = await getDocs(videosRef); + + if (snapshot.empty) { + console.log("No se encontraron videos "); + return []; + } + + return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })); +} + +// CREAR documentos +export async function addVideoToMaeteca(videoData, { invalidate = true } = {}) { + try { + const user = await getCurrentUser(); + if (!user) throw new Error('No authenticated user for write'); + + assertVideoPermissions(user); + + const payload = { + ...videoData, + createdBy: { uid: user.uid, role: user.role }, + createdAt: serverTimestamp() + }; + + const docRef = await addDoc(collection(firestoreDB, "videos"), payload); + if (invalidate) { + await invalidateVideoCaches(); + } + console.log("Documento agregado con ID:", docRef.id); + return docRef; + } catch (error) { + console.error("Error agregando documento:", error); + throw error; + } +} + + +// LEER todos los videos +export async function getAllVideos(options = {}) { + try { + return await withCache( + cacheKeys.videosAll(), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + fetchAllVideosFresh + ); + } catch (error) { + console.error("Error obteniendo videos:", error); + throw error; + } +} + +export function canUserManageVideos(role) { + return VIDEO_MANAGER_ROLES.includes(role ?? ''); +} + +// Constantes reutilizables por la UI +export const AVAILABLE_TAG_OPTIONS = [ + { label: '#maeteca', value: 'maeteca' }, + { label: '#general', value: 'general' }, + { label: '#tutorial', value: 'tutorial' }, + { label: '#matematicas', value: 'matematicas' }, + { label: '#Quimica', value: 'Quimica' }, + { label: '#Ciencias sociales', value: 'Ciencias sociales' }, + { label: '#Creatividad', value: 'Creatividad' }, + { label: '#Fisica', value: 'Fisica' } +]; + +export const TAGS = [ + { name: 'Programación', code: 'PROG' }, + { name: 'Matemáticas', code: 'MATH' }, + { name: 'Física', code: 'PHY' } +]; + +export const VIDEO_SUBJECTS = [ + { name: 'General', code: 'general' }, + { name: 'Matemáticas', code: 'math' }, + { name: 'Programación', code: 'prog' } +]; + +export const VIDEO_CAREERS = [ + { name: 'Todas', code: 'all' }, + { name: 'ITC', code: 'itc' }, + { name: 'IMT', code: 'imt' }, + { name: 'IDS', code: 'ids' } +]; + +export const SEMESTERS = [ + { name: 'Primer Semestre', code: '1' }, + { name: 'Segundo Semestre', code: '2' }, + { name: 'Tercer Semestre', code: '3' } +]; + +export const TYPES = [ + { name: 'Video', code: 'VID' }, + { name: 'Artículo', code: 'ART' }, + { name: 'Libro', code: 'BOOK' } +]; + +// Extrae el id de YouTube desde varias formas de URL +export function extractYoutubeId(url) { + if (!url || typeof url !== 'string') return null; + const patterns = [ + /youtube\.com\/watch\?v=([^&]+)/, + /youtube\.com\/embed\/([^?]+)/, + /youtu\.be\/([^?]+)/ + ]; + for (const p of patterns) { + const m = url.match(p); + if (m?.[1]) return m[1]; + } + return null; +} + +export async function loadMaetecaVideos(options = {}) { + const data = await getAllVideos(options); + return Array.isArray(data) ? data : []; +} + +// Obtener un video por su id de documento +export async function getVideoById(id, options = {}) { + if (!id) return null; + try { + return await withCache( + cacheKeys.videoById(id), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + async () => { + const videos = await getAllVideos(options); + return (videos ?? []).find((video) => video.id === id) ?? null; + } + ); + } catch (error) { + console.error(`Error obteniendo video ${id}:`, error); + throw error; + } +} + +// Genera la URL de embed (iframe) para un video de YouTube +export function getVideoEmbedUrl(video) { + if (!video) return null; + const url = typeof video === 'string' ? video : video.Video; + if (!url) return null; + const id = extractYoutubeId(url); + if (id) return `https://www.youtube.com/embed/${id}`; + return url; +} + +export function getVideoThumbnail(video) { + if (!video) return null; + if (video.Thumbnail) return video.Thumbnail; + const url = video.Video; + if (!url) return null; + const id = extractYoutubeId(url); + return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null; +} + +export function openVideo(url) { + if (!url || typeof window === 'undefined') return; + window.open(url, '_blank', 'noopener'); +} + +export function handleThumbnailKey(event, url) { + if (!url) return; + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + openVideo(url); + } +} + +// BUSCAR por array "Relacionado" +export async function getVideosByRelated(relacionadoItem, options = {}) { + try { + return await withCache( + cacheKeys.videosByRelated(relacionadoItem), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + async () => { + const videos = await getAllVideos(options); + return (videos ?? []).filter((video) => Array.isArray(video.Relacionado) && video.Relacionado.includes(relacionadoItem)); + } + ); + } catch (error) { + console.error("Error buscando por relacionado:", error); + throw error; + } +} + +// Actualiza un documento de video +// (removed optional admin/update/delete/filter/subscribe helpers per request) + + +export async function createSampleVideos() { + const user = await getCurrentUser(); + if (!user) throw new Error('No authenticated user for write'); + assertVideoPermissions(user); + console.log("Usuario autenticado:", user); // Verifica la información del usuario + + const samples = [ + { + "Informacion": "Video explicativo sobre el Método de Euler en modelación matemática.", + "Relacionado": ["método de euler", "modelación matemática", "aproximación numérica"], + "Titulo": "Método de Euler", + "Video": "https://youtu.be/B9YR-GXGncw" + }, + { + "Informacion": "Video sobre funciones polinomiales y cómo calcular su derivada.", + "Relacionado": ["funciones polinomiales", "derivadas", "modelación matemática"], + "Titulo": "Funciones polinomiales y su derivada", + "Video": "https://youtu.be/2ntPaw4vkc8" + }, + { + "Informacion": "Explicación de la función exponencial y el cálculo de su derivada.", + "Relacionado": ["función exponencial", "derivadas", "modelación matemática"], + "Titulo": "Función exponencial y su derivada", + "Video": "https://youtu.be/6V_LmXGCbSg" + }, + { + "Informacion": "Derivación de constantes y suma de funciones en modelación matemática.", + "Relacionado": ["derivadas", "constantes", "suma de funciones"], + "Titulo": "Derivada de constante y suma de funciones", + "Video": "https://youtu.be/9ghBNnZ6t7g" + }, + { + "Informacion": "Cómo derivar el producto de dos funciones.", + "Relacionado": ["derivadas", "producto de funciones", "regla del producto"], + "Titulo": "Derivada de producto de funciones", + "Video": "https://youtu.be/8XS55_kOlmk" + }, + { + "Informacion": "Derivación de cociente de funciones con ejemplos paso a paso.", + "Relacionado": ["derivadas", "cociente de funciones", "regla del cociente"], + "Titulo": "Derivada de cociente de funciones", + "Video": "https://youtu.be/cw9zLw6k3dA" + }, + { + "Informacion": "Uso de la regla de la cadena y derivación implícita.", + "Relacionado": ["regla de la cadena", "derivación implícita", "derivadas"], + "Titulo": "Regla de la cadena y derivación implícita", + "Video": "https://youtu.be/aGRJEaYh9Ws" + }, + { + "Informacion": "Cambio de variable en integración y derivación.", + "Relacionado": ["cambio de variable", "integrales", "derivadas"], + "Titulo": "Cambio de variable", + "Video": "https://youtu.be/pMLrvpBF_4o" + }, + { + "Informacion": "Integración por partes aplicada a problemas de ingeniería.", + "Relacionado": ["integrales", "integración por partes", "modelación matemática"], + "Titulo": "Integración por partes", + "Video": "https://youtu.be/VzxmmKKY3GM" + }, + { + "Informacion": "Concepto y cálculo del plano tangente en superficies.", + "Relacionado": ["plano tangente", "derivadas parciales", "superficies"], + "Titulo": "Plano tangente", + "Video": "https://youtu.be/ZzwEwfFTP7Q" + }, + { + "Informacion": "Cómo calcular derivadas direccionales y su interpretación geométrica.", + "Relacionado": ["derivada direccional", "gradiente", "superficies"], + "Titulo": "Derivada direccional", + "Video": "https://youtu.be/RbySC1xgM9o" + }, + { + "Informacion": "Uso de la transformada de Laplace en ecuaciones diferenciales.", + "Relacionado": ["transformada de laplace", "ecuaciones diferenciales", "modelación dinámica"], + "Titulo": "Transformada de Laplace", + "Video": "https://youtu.be/-7vsj9f24-c" + }, + { + "Informacion": "Conceptos básicos y operaciones con matrices.", + "Relacionado": ["matrices", "álgebra lineal", "operaciones matriciales"], + "Titulo": "Matrices: conceptos básicos y operaciones", + "Video": "https://youtu.be/krpLf9XP4vs" + }, + { + "Informacion": "Cómo calcular la matriz de cofactores.", + "Relacionado": ["matrices", "cofactores", "determinantes"], + "Titulo": "Matriz de cofactores", + "Video": "https://youtu.be/9uZ96OEcTuc" + }, + { + "Informacion": "Definición y obtención de la matriz adjunta.", + "Relacionado": ["matrices", "matriz adjunta", "álgebra lineal"], + "Titulo": "Matriz adjunta", + "Video": "https://youtu.be/PhJwWFWQQiY" + }, + { + "Informacion": "Cálculo de la matriz inversa paso a paso.", + "Relacionado": ["matrices", "inversa de matriz", "determinantes"], + "Titulo": "Matriz inversa", + "Video": "https://youtu.be/nyFLyIeeHmA" + }, + { + "Informacion": "Cálculo del determinante en sistemas de ecuaciones nxn.", + "Relacionado": ["determinantes", "sistemas lineales", "álgebra lineal"], + "Titulo": "Cálculo del determinante en sistemas nxn", + "Video": "https://youtu.be/XgWuTkx0CjA" + }, + { + "Informacion": "Resolución de sistemas lineales por el método de Gauss-Jordan.", + "Relacionado": ["gauss-jordan", "sistemas lineales", "álgebra lineal"], + "Titulo": "Método de Gauss-Jordan", + "Video": "https://youtu.be/MslG1TrSQO4" + }, + { + "Informacion": "Cálculo con operadores aplicado a ingeniería.", + "Relacionado": ["operadores", "pensamiento computacional", "matemáticas aplicadas"], + "Titulo": "Cálculo con operadores", + "Video": "https://youtu.be/AiJIcK3yIZw" + }, + { + "Informacion": "Uso del método Solver para resolver problemas de programación lineal.", + "Relacionado": ["programación lineal", "solver", "análisis de decisiones"], + "Titulo": "Método Solver en programación lineal", + "Video": "https://youtu.be/c-DPPmNef0Y" + }, + { + "Informacion": "Representación gráfica de modelos de programación lineal.", + "Relacionado": ["programación lineal", "gráficas", "análisis de decisiones"], + "Titulo": "Gráficas en programación lineal", + "Video": "https://youtu.be/RQ2pSyjH-64" + }, + { + "Informacion": "Uso del comando Array en AutoCAD.", + "Relacionado": ["autocad", "comando array", "dibujo asistido"], + "Titulo": "Comando Array", + "Video": "https://youtu.be/t3W_DDSnTDU" + }, + { + "Informacion": "Cómo usar el comando Offset para crear copias paralelas de objetos.", + "Relacionado": ["autocad", "offset", "diseño técnico"], + "Titulo": "Comando Offset", + "Video": "https://youtu.be/FXU5ZzXTwRQ" + }, + { + "Informacion": "Cálculo del área de una figura en AutoCAD.", + "Relacionado": ["autocad", "área", "medición"], + "Titulo": "Cálculo del área de una figura", + "Video": "https://youtu.be/IpBFI7Zhymg" + }, + { + "Informacion": "Uso de los comandos Trim y Fillet para edición de figuras.", + "Relacionado": ["autocad", "trim", "fillet", "dibujo 2D"], + "Titulo": "Comandos Trim y Fillet", + "Video": "https://youtu.be/fA-z6OjDvMQ" + } +]; + + const insertedIds = []; + try { + for (const item of samples) { + // addVideoToMaeteca will attach createdBy and createdAt + const ref = await addVideoToMaeteca(item, { invalidate: false }); + if (ref && ref.id) insertedIds.push(ref.id); + console.log(`Agregado: ${item.Titulo} -> ${ref?.id}`); + } + await invalidateVideoCaches(); + console.log(`Videos de ejemplo creados Total: ${insertedIds.length}`); + return insertedIds; + } catch (error) { + console.error("Error creando videos de ejemplo:", error); + throw error; + } +} + diff --git a/src/firebase/db/maeteca.js b/src/firebase/db/maeteca.js index 32bc62a56..c9a8da327 100644 --- a/src/firebase/db/maeteca.js +++ b/src/firebase/db/maeteca.js @@ -1,406 +1 @@ -// Filtra videos por texto en título o descripción, normalizado (MaesActivos style) -import { normalize } from '@/utils/HorarioUtils'; -export function filterVideosByText(videos, text) { - if (!Array.isArray(videos)) return []; - const normalizeText = typeof text === 'string' ? text : ''; - const query = normalize(normalizeText || ''); - if (!query) return videos; - return videos.filter(video => { - const title = normalize(video.Titulo || ''); - const info = normalize(video.Informacion || ''); - return title.includes(query) || info.includes(query); - }); -} -import { firestoreDB } from "../../main"; -import { - getDocs, - getDoc, - addDoc, - setDoc, - doc, - collection, - query, - where, - serverTimestamp -} from 'firebase/firestore'; -import { getCurrentUser } from './users'; - -export const VIDEO_MANAGER_ROLES = ['admin', 'tec', 'coordi']; - -function assertVideoPermissions(user) { - if (!VIDEO_MANAGER_ROLES.includes(user?.role)) { - const role = user?.role ?? 'unknown'; - throw new Error(`Insufficient permissions for role '${role}' to manage Maeteca videos`); - } -} - -// CREAR documentos -export async function addVideoToMaeteca(videoData) { - try { - const user = await getCurrentUser(); - if (!user) throw new Error('No authenticated user for write'); - - assertVideoPermissions(user); - - const payload = { - ...videoData, - createdBy: { uid: user.uid, role: user.role }, - createdAt: serverTimestamp() - }; - - const docRef = await addDoc(collection(firestoreDB, "videos"), payload); - console.log("Documento agregado con ID:", docRef.id); - return docRef; - } catch (error) { - console.error("Error agregando documento:", error); - throw error; - } -} - - -// LEER todos los videos -export async function getAllVideos() { - try { - const videosRef = collection(firestoreDB, "videos"); - const snapshot = await getDocs(videosRef); - - if (snapshot.empty) { - console.log("No se encontraron videos "); - return []; - } - - const videos = []; - snapshot.forEach(doc => { - videos.push({ id: doc.id, ...doc.data() }); - }); - - return videos; - } catch (error) { - console.error("Error obteniendo videos:", error); - throw error; - } -} - -export function canUserManageVideos(role) { - return VIDEO_MANAGER_ROLES.includes(role ?? ''); -} - -// Constantes reutilizables por la UI -export const AVAILABLE_TAG_OPTIONS = [ - { label: '#maeteca', value: 'maeteca' }, - { label: '#general', value: 'general' }, - { label: '#tutorial', value: 'tutorial' }, - { label: '#matematicas', value: 'matematicas' }, - { label: '#Quimica', value: 'Quimica' }, - { label: '#Ciencias sociales', value: 'Ciencias sociales' }, - { label: '#Creatividad', value: 'Creatividad' }, - { label: '#Fisica', value: 'Fisica' } -]; - -export const TAGS = [ - { name: 'Programación', code: 'PROG' }, - { name: 'Matemáticas', code: 'MATH' }, - { name: 'Física', code: 'PHY' } -]; - -export const VIDEO_SUBJECTS = [ - { name: 'General', code: 'general' }, - { name: 'Matemáticas', code: 'math' }, - { name: 'Programación', code: 'prog' } -]; - -export const VIDEO_CAREERS = [ - { name: 'Todas', code: 'all' }, - { name: 'ITC', code: 'itc' }, - { name: 'IMT', code: 'imt' }, - { name: 'IDS', code: 'ids' } -]; - -export const SEMESTERS = [ - { name: 'Primer Semestre', code: '1' }, - { name: 'Segundo Semestre', code: '2' }, - { name: 'Tercer Semestre', code: '3' } -]; - -export const TYPES = [ - { name: 'Video', code: 'VID' }, - { name: 'Artículo', code: 'ART' }, - { name: 'Libro', code: 'BOOK' } -]; - -// Extrae el id de YouTube desde varias formas de URL -export function extractYoutubeId(url) { - if (!url || typeof url !== 'string') return null; - const patterns = [ - /youtube\.com\/watch\?v=([^&]+)/, - /youtube\.com\/embed\/([^?]+)/, - /youtu\.be\/([^?]+)/ - ]; - for (const p of patterns) { - const m = url.match(p); - if (m?.[1]) return m[1]; - } - return null; -} - -export async function loadMaetecaVideos() { - const data = await getAllVideos(); - return Array.isArray(data) ? data : []; -} - -// Obtener un video por su id de documento -export async function getVideoById(id) { - if (!id) return null; - try { - const docRef = doc(firestoreDB, 'videos', id); - const snapshot = await getDoc(docRef); - if (!snapshot.exists()) { - console.log(`Documento con id ${id} no encontrado`); - return null; - } - return { id: snapshot.id, ...snapshot.data() }; - } catch (error) { - console.error(`Error obteniendo video ${id}:`, error); - throw error; - } -} - -// Genera la URL de embed (iframe) para un video de YouTube -export function getVideoEmbedUrl(video) { - if (!video) return null; - const url = typeof video === 'string' ? video : video.Video; - if (!url) return null; - const id = extractYoutubeId(url); - if (id) return `https://www.youtube.com/embed/${id}`; - return url; -} - -export function getVideoThumbnail(video) { - if (!video) return null; - if (video.Thumbnail) return video.Thumbnail; - const url = video.Video; - if (!url) return null; - const id = extractYoutubeId(url); - return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null; -} - -export function openVideo(url) { - if (!url || typeof window === 'undefined') return; - window.open(url, '_blank', 'noopener'); -} - -export function handleThumbnailKey(event, url) { - if (!url) return; - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openVideo(url); - } -} - -// BUSCAR por array "Relacionado" -export async function getVideosByRelated(relacionadoItem) { - try { - const videosRef = collection(firestoreDB, "videos"); - const q = query( - videosRef, - where("Relacionado", "array-contains", relacionadoItem) - ); - - const snapshot = await getDocs(q); - - if (snapshot.empty) { - console.log("No se encontraron documentos "); - return []; - } - - const videos = []; - snapshot.forEach(doc => { - videos.push({ id: doc.id, ...doc.data() }); - }); - - return videos; - } catch (error) { - console.error("Error buscando por relacionado:", error); - throw error; - } -} - -// Actualiza un documento de video -// (removed optional admin/update/delete/filter/subscribe helpers per request) - - -export async function createSampleVideos() { - const user = await getCurrentUser(); - if (!user) throw new Error('No authenticated user for write'); - assertVideoPermissions(user); - console.log("Usuario autenticado:", user); // Verifica la información del usuario - - const samples = [ - { - "Informacion": "Video explicativo sobre el Método de Euler en modelación matemática.", - "Relacionado": ["método de euler", "modelación matemática", "aproximación numérica"], - "Titulo": "Método de Euler", - "Video": "https://youtu.be/B9YR-GXGncw" - }, - { - "Informacion": "Video sobre funciones polinomiales y cómo calcular su derivada.", - "Relacionado": ["funciones polinomiales", "derivadas", "modelación matemática"], - "Titulo": "Funciones polinomiales y su derivada", - "Video": "https://youtu.be/2ntPaw4vkc8" - }, - { - "Informacion": "Explicación de la función exponencial y el cálculo de su derivada.", - "Relacionado": ["función exponencial", "derivadas", "modelación matemática"], - "Titulo": "Función exponencial y su derivada", - "Video": "https://youtu.be/6V_LmXGCbSg" - }, - { - "Informacion": "Derivación de constantes y suma de funciones en modelación matemática.", - "Relacionado": ["derivadas", "constantes", "suma de funciones"], - "Titulo": "Derivada de constante y suma de funciones", - "Video": "https://youtu.be/9ghBNnZ6t7g" - }, - { - "Informacion": "Cómo derivar el producto de dos funciones.", - "Relacionado": ["derivadas", "producto de funciones", "regla del producto"], - "Titulo": "Derivada de producto de funciones", - "Video": "https://youtu.be/8XS55_kOlmk" - }, - { - "Informacion": "Derivación de cociente de funciones con ejemplos paso a paso.", - "Relacionado": ["derivadas", "cociente de funciones", "regla del cociente"], - "Titulo": "Derivada de cociente de funciones", - "Video": "https://youtu.be/cw9zLw6k3dA" - }, - { - "Informacion": "Uso de la regla de la cadena y derivación implícita.", - "Relacionado": ["regla de la cadena", "derivación implícita", "derivadas"], - "Titulo": "Regla de la cadena y derivación implícita", - "Video": "https://youtu.be/aGRJEaYh9Ws" - }, - { - "Informacion": "Cambio de variable en integración y derivación.", - "Relacionado": ["cambio de variable", "integrales", "derivadas"], - "Titulo": "Cambio de variable", - "Video": "https://youtu.be/pMLrvpBF_4o" - }, - { - "Informacion": "Integración por partes aplicada a problemas de ingeniería.", - "Relacionado": ["integrales", "integración por partes", "modelación matemática"], - "Titulo": "Integración por partes", - "Video": "https://youtu.be/VzxmmKKY3GM" - }, - { - "Informacion": "Concepto y cálculo del plano tangente en superficies.", - "Relacionado": ["plano tangente", "derivadas parciales", "superficies"], - "Titulo": "Plano tangente", - "Video": "https://youtu.be/ZzwEwfFTP7Q" - }, - { - "Informacion": "Cómo calcular derivadas direccionales y su interpretación geométrica.", - "Relacionado": ["derivada direccional", "gradiente", "superficies"], - "Titulo": "Derivada direccional", - "Video": "https://youtu.be/RbySC1xgM9o" - }, - { - "Informacion": "Uso de la transformada de Laplace en ecuaciones diferenciales.", - "Relacionado": ["transformada de laplace", "ecuaciones diferenciales", "modelación dinámica"], - "Titulo": "Transformada de Laplace", - "Video": "https://youtu.be/-7vsj9f24-c" - }, - { - "Informacion": "Conceptos básicos y operaciones con matrices.", - "Relacionado": ["matrices", "álgebra lineal", "operaciones matriciales"], - "Titulo": "Matrices: conceptos básicos y operaciones", - "Video": "https://youtu.be/krpLf9XP4vs" - }, - { - "Informacion": "Cómo calcular la matriz de cofactores.", - "Relacionado": ["matrices", "cofactores", "determinantes"], - "Titulo": "Matriz de cofactores", - "Video": "https://youtu.be/9uZ96OEcTuc" - }, - { - "Informacion": "Definición y obtención de la matriz adjunta.", - "Relacionado": ["matrices", "matriz adjunta", "álgebra lineal"], - "Titulo": "Matriz adjunta", - "Video": "https://youtu.be/PhJwWFWQQiY" - }, - { - "Informacion": "Cálculo de la matriz inversa paso a paso.", - "Relacionado": ["matrices", "inversa de matriz", "determinantes"], - "Titulo": "Matriz inversa", - "Video": "https://youtu.be/nyFLyIeeHmA" - }, - { - "Informacion": "Cálculo del determinante en sistemas de ecuaciones nxn.", - "Relacionado": ["determinantes", "sistemas lineales", "álgebra lineal"], - "Titulo": "Cálculo del determinante en sistemas nxn", - "Video": "https://youtu.be/XgWuTkx0CjA" - }, - { - "Informacion": "Resolución de sistemas lineales por el método de Gauss-Jordan.", - "Relacionado": ["gauss-jordan", "sistemas lineales", "álgebra lineal"], - "Titulo": "Método de Gauss-Jordan", - "Video": "https://youtu.be/MslG1TrSQO4" - }, - { - "Informacion": "Cálculo con operadores aplicado a ingeniería.", - "Relacionado": ["operadores", "pensamiento computacional", "matemáticas aplicadas"], - "Titulo": "Cálculo con operadores", - "Video": "https://youtu.be/AiJIcK3yIZw" - }, - { - "Informacion": "Uso del método Solver para resolver problemas de programación lineal.", - "Relacionado": ["programación lineal", "solver", "análisis de decisiones"], - "Titulo": "Método Solver en programación lineal", - "Video": "https://youtu.be/c-DPPmNef0Y" - }, - { - "Informacion": "Representación gráfica de modelos de programación lineal.", - "Relacionado": ["programación lineal", "gráficas", "análisis de decisiones"], - "Titulo": "Gráficas en programación lineal", - "Video": "https://youtu.be/RQ2pSyjH-64" - }, - { - "Informacion": "Uso del comando Array en AutoCAD.", - "Relacionado": ["autocad", "comando array", "dibujo asistido"], - "Titulo": "Comando Array", - "Video": "https://youtu.be/t3W_DDSnTDU" - }, - { - "Informacion": "Cómo usar el comando Offset para crear copias paralelas de objetos.", - "Relacionado": ["autocad", "offset", "diseño técnico"], - "Titulo": "Comando Offset", - "Video": "https://youtu.be/FXU5ZzXTwRQ" - }, - { - "Informacion": "Cálculo del área de una figura en AutoCAD.", - "Relacionado": ["autocad", "área", "medición"], - "Titulo": "Cálculo del área de una figura", - "Video": "https://youtu.be/IpBFI7Zhymg" - }, - { - "Informacion": "Uso de los comandos Trim y Fillet para edición de figuras.", - "Relacionado": ["autocad", "trim", "fillet", "dibujo 2D"], - "Titulo": "Comandos Trim y Fillet", - "Video": "https://youtu.be/fA-z6OjDvMQ" - } -]; - - const insertedIds = []; - try { - for (const item of samples) { - // addVideoToMaeteca will attach createdBy and createdAt - const ref = await addVideoToMaeteca(item); - if (ref && ref.id) insertedIds.push(ref.id); - console.log(`Agregado: ${item.Titulo} -> ${ref?.id}`); - } - console.log(`Videos de ejemplo creados Total: ${insertedIds.length}`); - return insertedIds; - } catch (error) { - console.error("Error creando videos de ejemplo:", error); - throw error; - } -} - +export * from './maeteca.cached'; diff --git a/src/firebase/db/majors.cached.js b/src/firebase/db/majors.cached.js new file mode 100644 index 000000000..9978e16db --- /dev/null +++ b/src/firebase/db/majors.cached.js @@ -0,0 +1,27 @@ +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, +} from 'firebase/firestore'; +import { withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getMajors(options = {}) { + return await withCache( + cacheKeys.majors(), + { + ttlMs: CACHE_TTL_MS.MAJORS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.MAJORS] + }, + async () => { + const majorsRef = collection(firestoreDB, "schools/tec.mx/majors"); + const docsSnap = await getDocs(majorsRef); + if (docsSnap) { + return docsSnap.docs.map(doc => doc.data()); + } + return null; + } + ); +} diff --git a/src/firebase/db/majors.js b/src/firebase/db/majors.js index a64c35f0b..59fdff405 100644 --- a/src/firebase/db/majors.js +++ b/src/firebase/db/majors.js @@ -1,17 +1 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, -} from 'firebase/firestore'; - -export async function getMajors() { - const majorsRef = collection(firestoreDB, "schools/tec.mx/majors"); - - const docsSnap = await getDocs(majorsRef); - - if (docsSnap) { - return docsSnap.docs.map(doc => doc.data()); - } else { - return null; - } -} \ No newline at end of file +export * from './majors.cached'; diff --git a/src/firebase/db/subjects.cached.js b/src/firebase/db/subjects.cached.js new file mode 100644 index 000000000..55b1e5101 --- /dev/null +++ b/src/firebase/db/subjects.cached.js @@ -0,0 +1,45 @@ +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, + query, + orderBy, +} from 'firebase/firestore'; +import { doc, setDoc, deleteDoc } from 'firebase/firestore'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getSubjects(options = {}) { + return await withCache( + cacheKeys.subjects(), + { + ttlMs: CACHE_TTL_MS.SUBJECTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.SUBJECTS] + }, + async () => { + const subjectsRef = collection(firestoreDB, "schools/tec.mx/subjects"); + const q = query(subjectsRef, orderBy("name")); + const docsSnap = await getDocs(q); + + if (!docsSnap.empty) { + return docsSnap.docs.map(doc => doc.data()); + } + + return null; + } + ); +} + +export async function addSubject(subject) { + const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subject.id}`); + await setDoc(subjectRef, subject); + await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); +} + +export async function deleteSubject(subjectId) { + const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subjectId}`); + await deleteDoc(subjectRef); + await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); + } diff --git a/src/firebase/db/subjects.js b/src/firebase/db/subjects.js index a77f62d19..6eba559ac 100644 --- a/src/firebase/db/subjects.js +++ b/src/firebase/db/subjects.js @@ -1,32 +1 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, - query, - orderBy, -} from 'firebase/firestore'; -import { doc, setDoc, deleteDoc } from 'firebase/firestore'; - -export async function getSubjects() { - const subjectsRef = collection(firestoreDB, "schools/tec.mx/subjects"); - - const q = query(subjectsRef, orderBy("name")); - - const docsSnap = await getDocs(q); - - if (!docsSnap.empty) { - return docsSnap.docs.map(doc => doc.data()); - } else { - return null; - } -} - -export async function addSubject(subject) { - const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subject.id}`); - await setDoc(subjectRef, subject); -} - -export async function deleteSubject(subjectId) { - const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subjectId}`); - await deleteDoc(subjectRef); - } \ No newline at end of file +export * from './subjects.cached'; diff --git a/src/firebase/db/users.cached.js b/src/firebase/db/users.cached.js new file mode 100644 index 000000000..c1bf6255c --- /dev/null +++ b/src/firebase/db/users.cached.js @@ -0,0 +1,1038 @@ +import { firestoreDB } from "../../main"; +import { getAuth } from 'firebase/auth'; +import { + doc, + collection, + query, + where, + setDoc, + getDoc, + getDocs, + updateDoc, + serverTimestamp, + deleteField, + increment, + getFirestore, +} from 'firebase/firestore'; +import { getUserProfilePicture } from "../img/users"; +import * as XLSX from 'xlsx'; +import { writeBatch } from "firebase/firestore"; +import { invalidateCacheTags, withCache } from "../cache/cache"; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys, userTag } from "../cache/config"; + +const db = getFirestore(); +const MAE_DIRECTORY_ROLES = ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi', 'tec']; + + +function getEmailUsername(email) { + var atIndex = email.indexOf('@'); + if (atIndex !== -1) { + return email.slice(0, atIndex); + } + return null; +} + +function getCurrentDayKey() { + return ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][new Date().getDay()]; +} + +function sortUsersByClosestSchedule(data) { + const today = new Date().getDay(); + + data.sort((a, b) => { + const { day: dayA, startTime: startTimeA } = getClosestDayAndStartTime(a.weekSchedule); + const { day: dayB, startTime: startTimeB } = getClosestDayAndStartTime(b.weekSchedule); + + const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + const daysOrdered = [...daysOfWeek.slice(today), ...daysOfWeek.slice(0, today)]; + + const dayIndexA = daysOrdered.indexOf(dayA); + const dayIndexB = daysOrdered.indexOf(dayB); + const dayComparison = (dayIndexA === -1 ? 1 : (dayIndexB === -1 ? -1 : dayIndexA - dayIndexB)); + if (dayComparison !== 0) return dayComparison; + + const startTimeComparison = (startTimeA === null ? 1 : (startTimeB === null ? -1 : startTimeA.localeCompare(startTimeB))); + if (startTimeComparison !== 0) return startTimeComparison; + + return a.name.localeCompare(b.name); + }); + + return data; +} + +async function fetchMaeDirectoryFresh() { + const usersRef = collection(firestoreDB, "users"); + const q = query(usersRef, where('role', 'in', MAE_DIRECTORY_ROLES)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot) { + return null; + } + + let data = querySnapshot.docs.map(doc => doc.data()).filter(item => item.name); + + data = await Promise.all(data.map(async (item) => { + const profilePictureUrl = await getUserProfilePicture(item.email); + return { ...item, profilePictureUrl }; + })); + + return sortUsersByClosestSchedule(data); +} + +async function getMaeDirectory(options = {}) { + return await withCache( + cacheKeys.maeDirectory(), + { + ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] + }, + fetchMaeDirectoryFresh + ); +} + +function getSemesterCacheKey() { + const now = new Date(); + const semester = now.getMonth() < 6 ? '01' : '02'; + return `${now.getFullYear()}-${semester}`; +} + +async function invalidateUserCaches(userId, { includeActive = false, includeLeaderboard = false } = {}) { + const tags = [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, CACHE_TAGS.CURRENT_USER, CACHE_TAGS.MAES]; + + if (userId) { + tags.push(userTag(userId)); + } + if (includeActive) { + tags.push(CACHE_TAGS.ACTIVE_MAES); + } + if (includeLeaderboard) { + tags.push(CACHE_TAGS.LEADERBOARD); + } + + await invalidateCacheTags(tags); +} + +export async function createUser(userInfo) { + + userInfo.id = getEmailUsername(userInfo.email); + userInfo.uid = getEmailUsername(userInfo.email); + + userInfo.career = userInfo.major.id + userInfo.area = userInfo.major.school + + userInfo.name = userInfo.firstname.trim() + ' ' + userInfo.lastname.trim(); + + const userRef = doc(firestoreDB, "users", userInfo.uid); + const result = await setDoc(userRef, userInfo); + await invalidateUserCaches(userInfo.uid); + return result; +} + +export async function getUser(uid, options = {}) { + return await withCache( + cacheKeys.userById(uid), + { + ttlMs: CACHE_TTL_MS.USER, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, userTag(uid)] + }, + async () => { + const userRef = doc(firestoreDB, "users", uid); + const docSnap = await getDoc(userRef); + + if (docSnap.exists()) { + const data = docSnap.data(); + const profilePictureUrl = await getUserProfilePicture(data.email); + return { ...data, profilePictureUrl }; + } + + return null; + } + ); +} + +export async function getCurrentUser(options = {}) { + const auth = getAuth(); + if (auth.currentUser) { + const uid = getEmailUsername(auth.currentUser.email); + return await withCache( + cacheKeys.currentUser(uid), + { + ttlMs: CACHE_TTL_MS.CURRENT_USER, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.CURRENT_USER, userTag(uid)] + }, + async () => await getUser(uid, options) + ); + } + return null; +} + +// Función para obtener el día más cercano en la semana y la hora de inicio más temprana +export const getClosestDayAndStartTime = (schedules) => { + if (typeof schedules !== 'object' || schedules === null || Array.isArray(schedules)) { + console.error('Expected a map of schedules, but received:', schedules); + return { day: null, startTime: null }; + } + + const today = new Date().getDay(); // Día actual (0-6) donde 0 es domingo + const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + + // Crear dos arrays, uno para los días futuros y otro para los pasados + const futureDays = daysOfWeek.slice(today); + const pastDays = daysOfWeek.slice(0, today); + + let closestDay = null; + let earliestStartTime = null; + + // Buscar primero entre los días futuros (desde hoy hasta el final de la semana) + futureDays.forEach(day => { + if (Array.isArray(schedules[day])) { + schedules[day].forEach(schedule => { + if (schedule.start) { + if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { + closestDay = day; + earliestStartTime = schedule.start; + } + } + }); + } + }); + + // Si no se encontró ningún día en el futuro, buscar en los días pasados (inicio de semana hasta hoy) + if (closestDay === null) { + pastDays.forEach(day => { + if (Array.isArray(schedules[day])) { + schedules[day].forEach(schedule => { + if (schedule.start) { + if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { + closestDay = day; + earliestStartTime = schedule.start; + } + } + }); + } + }); + } + + return { day: closestDay, startTime: earliestStartTime }; +}; + + +export async function getMaes(options = {}) { + return await getMaeDirectory(options); +} + +export async function getMaesNames(options = {}) { + return await getMaeDirectory(options); +} + + +export async function getUsersWithActiveSession(getProfilePicture = false, options = {}) { + try { + return await withCache( + cacheKeys.activeMaes(getProfilePicture ? 'with-photo' : 'basic'), + { + ttlMs: CACHE_TTL_MS.ACTIVE_MAES, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.ACTIVE_MAES] + }, + async () => { + const usersRef = collection(firestoreDB, "users"); + const q = query(usersRef, where('activeSession', '!=', null)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot) { + return null; + } + + const fiveHoursAgoTimestampSeconds = Math.floor(Date.now() / 1000) - 18000; + const filteredDocs = querySnapshot.docs.filter((doc) => { + const data = doc.data(); + return data.activeSession?.startTime?.seconds > fiveHoursAgoTimestampSeconds; + }); + + return await Promise.all(filteredDocs.map(async (doc) => { + const data = doc.data(); + const profilePictureUrl = getProfilePicture ? await getUserProfilePicture(data.email) : null; + return { ...data, ...(profilePictureUrl ? { profilePictureUrl } : {}) }; + })); + } + ); + } catch (error) { + console.error('Error retrieving users:', error); + } +}; + +export async function updateUserInfo(userId, userInfo) { + userInfo['name'] = userInfo['firstname'].trim() + ' ' + userInfo['lastname'].trim() + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, userInfo); + await invalidateUserCaches(userId); + return result; +} + +export async function updateUserSubjects(userId, newSubjects) { + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, { + subjects: newSubjects + }); + await invalidateUserCaches(userId); + return result; +} + +export async function updateUserSchedule(userId, newSchedule) { + const userRef = doc(firestoreDB, "users", userId); + // Iterate over object keys + for (const day in newSchedule) { + // Check if the value is an empty array + if (Array.isArray(newSchedule[day]) && newSchedule[day].length === 0) { + // Delete the key with an empty array value + delete newSchedule[day]; + } + } + const result = await updateDoc(userRef, { + weekSchedule: newSchedule + }); + await invalidateUserCaches(userId); + return result; +} + +export async function getTodaysMae(options = {}) { + try { + return await withCache( + cacheKeys.maesToday(getCurrentDayKey()), + { + ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] + }, + async () => { + const users = await getMaeDirectory(options); + const currentDay = getCurrentDayKey(); + + return (users ?? []) + .filter((user) => user.weekSchedule && user.weekSchedule[currentDay]) + .sort((a, b) => { + const aStartTime = a.weekSchedule[currentDay][0]?.start; + const bStartTime = b.weekSchedule[currentDay][0]?.start; + const aTime = aStartTime ? new Date(`1970-01-01T${aStartTime}:00Z`) : new Date(); + const bTime = bStartTime ? new Date(`1970-01-01T${bStartTime}:00Z`) : new Date(); + return aTime - bTime; + }); + } + ); + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +export async function startActiveSession(userId, userInfo, location) { + try { + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, { + activeSession: { + peerInfo: userInfo, + location, + status: 'PENDING', + startTime: serverTimestamp(), + } + }); + await invalidateUserCaches(userId, { includeActive: true }); + return result; + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +export async function stopActiveSession(userId) { + try { + const userRef = doc(firestoreDB, "users", userId); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + throw new Error("User not found"); + } + + // Gets start time from current Active Session + const userData = userDoc.data(); + const startTime = userData.activeSession?.startTime?.toDate(); + + if (!startTime) { + throw new Error("Active session start time not found"); + } + + // Calculates and adds the duration of the current session to the total time + const currentTime = new Date(); + const differenceInMinutes = Math.floor((currentTime - startTime) / (1000 * 60)); + + if (differenceInMinutes > 310) { + await updateDoc(userRef, { + activeSession: deleteField() + }); + await invalidateUserCaches(userId, { includeActive: true }); + return { timeLimitExceded: true, activeSessionDeleted: false, differenceInMinutes } + } + + const totalTime = (userData.totalTime || 0) + differenceInMinutes; + + // Updates the total time and stops current session + await updateDoc(userRef, { + totalTime: totalTime, + activeSession: deleteField() + }); + await invalidateUserCaches(userId, { includeActive: true, includeLeaderboard: true }); + + return { totalTime, differenceInMinutes, activeSessionDeleted: true }; + } catch (error) { + return { activeSessionDeleted: false }; + } +} + +export async function incrementTotalTime(userId, time) { + const userRef = doc(firestoreDB, "users", userId); + + await updateDoc(userRef, { + totalTime: increment(time*60) + }); + await invalidateUserCaches(userId, { includeLeaderboard: true }); +} + + +export async function updateUserProfilePicture(userId, photoURL) { + try { + const userRef = doc(firestoreDB, 'users', userId); + + await updateDoc(userRef, { + photoURL: photoURL + }); + await invalidateUserCaches(userId); + + } catch (error) { + console.error('Error updating user profile picture: ', error); + throw error; + } +} + +/** + * Clears the content of the weekSchedule field for users with specific roles (admin, coordi, mae), + * but keeps the field as an empty object. + * + * @returns {Promise} - A promise that resolves when all eligible weekSchedules are cleared. + */ +export async function clearAllUsersWeekSchedule() { + try { + + const usersRef = collection(firestoreDB, "users"); + + const querySnapshot = await getDocs(usersRef); + + + const eligibleRoles = ['admin', 'coordi', 'mae','tec','publi']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + weekSchedule: {} + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Week schedule content has been successfully cleared for eligible users."); + } catch (error) { + console.error("Error clearing weekSchedule content for eligible users: ", error); + throw error; + } +} + +export async function checkAndUpdateUserRole(file = null) { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + if (file) { + // Leer y procesar el archivo Excel acuerdate que empieza a contar desde 0 + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const data = new Uint8Array(event.target.result); + const workbook = XLSX.read(data, { type: 'array' }); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + // console.log(sheet) + const excelData = XLSX.utils.sheet_to_json(sheet, { header: 0 }); + console.log(excelData) + // Convertimos las matrículas del Excel a correos en formato lowercase@tec.mx + const emailsFromExcel = excelData + .map(row => row["Matrícula"]?.toLowerCase() + "@tec.mx") + + console.log(emailsFromExcel); + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + const eligibleRoles = ['mae', 'coordi', 'publi', 'tec', 'admin']; + + const userEmail = userData.email?.toLowerCase(); + const userMatricula = userData.matricula?.toLowerCase(); + + if (eligibleRoles.includes(userData.role)) { + if (!emailsFromExcel.includes(userEmail)) { + return updateDoc(userRef, { role: "exmae" }); + } + } else { + // Si el usuario no tiene un rol elegible, se le asigna "mae" con estado "becario" + if (emailsFromExcel.includes(userEmail)) { + return updateUserToMae({ + matricula: userMatricula, + role: "mae", + status: "becario", + point: 0, + useCoins: 0, + }); + } + } + + return Promise.resolve(); + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + //console.log("Roles actualizados con base en el archivo Excel."); + } catch (error) { + console.error("Error al procesar el archivo Excel:", error); + throw error; + } + }; + + reader.readAsArrayBuffer(file); + } else { + // Si no hay archivo, ejecutamos la lógica normal + const eligibleRoles = ['mae', 'coordi', 'publi','tec']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + if (eligibleRoles.includes(userData.role)) { + const isWeekScheduleEmpty = Object.values(userData.weekSchedule).every(day => day.length === 0); + const isTotalTimeEquals0 = userData.totalTime == 0; + const hasNoSubjects = !userData.subjects || userData.subjects.length === 0; + if (isWeekScheduleEmpty && isTotalTimeEquals0 && hasNoSubjects) { + return updateDoc(userRef, { role: "exmae" }); + } + } + + return Promise.resolve(); + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Roles actualizados con base en weekSchedule, totalTime, y subjects."); + } + } catch (error) { + console.error("Error actualizando roles de los usuarios: ", error); + throw error; + } +} + +export async function updateUserToMae(data) { + const { role, matricula, status } = data; + const badges = [ + { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, + { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, + { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, + { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, + { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, + { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, + { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, + { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, + { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, + { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, + { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, + { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, + { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, + { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, + { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, + { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, + { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, + { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } + ]; + + + + const background = [ + { "id": "1", "image_url": "/assets/back/1.svg", "bought": true, "price": 0 }, + { "id": "2", "image_url": "/assets/back/2.svg", "bought": false, "price": 25 }, + { "id": "3", "image_url": "/assets/back/3.svg", "bought": false, "price": 25}, + { "id": "4", "image_url": "/assets/back/4.svg", "bought": false, "price": 25 }, + { "id": "5", "image_url": "/assets/back/5.svg", "bought": false, "price": 50 }, + { "id": "6", "image_url": "/assets/back/6.svg", "bought": false, "price": 50 }, + { "id": "7", "image_url": "/assets/back/7.svg", "bought": false, "price": 75 }, + { "id": "8", "image_url": "/assets/back/8.svg", "bought": false, "price": 100 }, + ]; + + if (!role || !matricula || !status) { + throw new Error("role, matricula, and status are required fields."); + } + + try { + const usersRef = collection(firestoreDB, "users"); + const userQuery = query(usersRef, where("email", "==", `${matricula.toLowerCase()}@tec.mx`)); + const querySnapshot = await getDocs(userQuery); + + if (querySnapshot.empty) { + console.log("No user found with the given matricula."); + return; + } + + // Procesar cada usuario encontrado + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (userData.role === 'user' || userData.status === 'estudiante') { + return updateDoc(userRef, { + role: role.value, + status: status.value, + weekSchedule: {}, + subjects: [], + totalTime: 0, + badges: badges, + points: 0, + useCoins: 0, + background: background, + }); + } else { + + return updateDoc(userRef, { + role: role.value, + status: status.value + }); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Usuarios actualizados exitosamente."); + } catch (error) { + console.error("Error al actualizar los usuarios: ", error); + } +} + +export const saveScheduleSubjectsExperience = async () => { + try { + const usersRef = collection(db, 'users'); + const usersSnap = await getDocs(usersRef); + + if (usersSnap.empty) { + console.error("No se encontraron usuarios en la tabla 'users'."); + return; + } + + const rolesPermitidos = ['admin', 'publi', 'mae', 'coordi', 'tec']; + + const updatePromises = []; + usersSnap.forEach(async (userDoc) => { + const user = userDoc.data(); + + if (!rolesPermitidos.includes(user.role)) { + return; + } + + let puntos = 0; + + if (user.subjects && user.subjects.length > 0) { + puntos += 15; + } else { + puntos -= 30; + await updateUserAchievementBadge(user.uid, "18"); + } + + if (user.weekSchedule && Object.keys(user.weekSchedule).length > 0) { + puntos += 100; + } else { + puntos -= 500; + await updateUserAchievementBadge(user.uid, "18"); + } + + const userRef = doc(db, 'users', userDoc.id); + updatePromises.push( + updateDoc(userRef, { + points: (user.points || 0) + puntos + }) + ); + }); + + await Promise.all(updatePromises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + } catch (error) { + console.error("Error al guardar la experiencia:", error); + } +}; + + +export async function updatePoints(uid, newPoints) { + const userRef = doc(db, 'users', uid); + const userSnap = await getDoc(userRef); + + if (!userSnap.exists()) { + console.log(`Usuario con uid ${uid} no encontrado.`); + return []; + } + + const user = userSnap.data(); + const updatedPoints = (user.points || 0) + newPoints; + + await updateDoc(userRef, { points: updatedPoints }); + if (newPoints < 0) { + await updateUserAchievementBadge(uid, "18"); + } + + await invalidateUserCaches(uid, { includeLeaderboard: true }); + return [{ id: uid, ...user, points: updatedPoints }]; +} + +export async function getExperience(options = {}) { + return await withCache( + cacheKeys.leaderboard(), + { + ttlMs: CACHE_TTL_MS.LEADERBOARD, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.LEADERBOARD] + }, + async () => { + const data = await getMaeDirectory(options); + return (data ?? []).slice().sort((a, b) => (b.points || 0) - (a.points || 0)); + } + ); +} + +// Funcion especial si mas adelante quieren agregar logros +export async function addBadgesToEligibleUsers() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const badges = [ + { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, + { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, + { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, + { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, + { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, + { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, + { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, + { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, + { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, + { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, + { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, + { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, + { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, + { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, + { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, + { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, + { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, + { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } + ]; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + badges: badges + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Badges have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding badges to eligible users: ", error); + throw error; + } +} + + +// actualizar le achieved del usuario +export async function updateUserAchievementBadge(uid, badgeId) { + try { + + const userRef = doc(firestoreDB, "users", uid); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.error("Usuario no encontrado"); + return; + } + + const userData = userDoc.data(); + const badges = userData.badges || []; + + const updatedBadges = badges.map((badge) => { + if (badge.id === badgeId) { + return { ...badge, achieved: true }; + } + return badge; + }); + + await updateDoc(userRef, { + badges: updatedBadges + }); + await invalidateUserCaches(uid, { includeLeaderboard: true }); + + // console.log(`El logro con id ${badgeId} se ha actualizado correctamente para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el logro del usuario:", error); + throw error; + } +} + + +// Contador de badges +export async function countAchievedBadges(uid) { + try { + const user = await getUser(uid); + + if (!user) { + console.error("Usuario no encontrado"); + return 0; + } + + const badges = user.badges || []; + return badges.reduce((count, badge) => count + (badge.achieved ? 1 : 0), 0); + } catch (error) { + console.error("Error al contar los logros alcanzados:", error); + throw error; + } +} + +// A??adir nuevos backgrounds a los usuarios sin borrar los existentes +export async function addBackgroundUsers() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const newBackgrounds = [ + { id: '8', image_url: '/assets/back/8.svg', bought: false, price: 100 }, + // Aquí puedes agregar más fondos nuevos... + ]; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + const currentBackgrounds = userData.background || []; + + // Crear un mapa de fondos existentes (para evitar duplicados) + const backgroundMap = new Map(currentBackgrounds.map(bg => [bg.id, bg])); + + // Añadir los nuevos fondos solo si no existen ya + newBackgrounds.forEach(bg => { + if (!backgroundMap.has(bg.id)) { + backgroundMap.set(bg.id, bg); + } + }); + + const mergedBackgrounds = Array.from(backgroundMap.values()); + + return updateDoc(userRef, { + background: mergedBackgrounds, + myBackground: userData.myBackground || "/assets/back/1.svg", // conservar el que ya tiene + useCoins: userData.useCoins || 0, // conservar las monedas que ya tiene + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Backgrounds have been merged successfully for eligible users."); + } catch (error) { + console.error("Error merging backgrounds for eligible users: ", error); + throw error; +} +} + + + +// actualizar le achieved del usuario +export async function updateUserBackground(uid, backId, coins, userCoins) { + try { + const userRef = doc(firestoreDB, "users", uid); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.error("Usuario no encontrado"); + return; + } + + const userData = userDoc.data(); + const background = userData.background || []; + + const updatedBackground = background.map((back) => { + if (back.id === backId) { + return { ...back, bought: true }; + } + return back; + }); + + await updateDoc(userRef, { + background: updatedBackground, + useCoins: userCoins + coins + }); + await invalidateUserCaches(uid, { includeLeaderboard: true }); + + console.log(`El fondo con id ${backId} se ha actualizado correctamente para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el fondo del usuario:", error); + throw error; + } +} + +// Actualizar fondo +export async function updateUserBackgroundImage(uid, backgroundUrl) { + try { + const userRef = doc(firestoreDB, "users", uid); + await updateDoc(userRef, { + myBackground: backgroundUrl + }); + await invalidateUserCaches(uid); + console.log(`El fondo se ha actualizado a ${backgroundUrl} para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el fondo del usuario:", error); + throw error; + } +} + + +export async function getTotalMaes(options = {}) { + try { + const maes = await getMaeDirectory(options); + return (maes ?? []).filter((user) => user.uid !== 'jackpot').length; + } catch (error) { + console.error("Error al obtener el total de MAEs: ", error); + throw error; + } +} + + + +// Añadir nuevas variables +export async function addExtraVariables() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + asesoriasGrupales: 0, + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Background have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding background to eligible users: ", error); + throw error; + } +} + + + +export async function clearUsersData() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "users")); + const rolesToUpdate = ["admin", "coordi", "mae", "tec", "publi"]; + + const updatePromises = []; + + querySnapshot.forEach(docSnapshot => { + const userData = docSnapshot.data(); + if (rolesToUpdate.includes(userData.role)) { + const userRef = doc(firestoreDB, "users", docSnapshot.id); + updatePromises.push(updateDoc(userRef, { + useCoins: userData.useCoins - userData.points, + subjects: [], + totalTime: 0, + points: 0 + })); + } + }); + + await Promise.all(updatePromises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Usuarios actualizados correctamente."); + } catch (error) { + console.error("Error al actualizar usuarios: ", error); + throw error; + } +} + + +export async function resetAllUsersTotalTimeAndPoints({ dryRun = false, batchSize = 450 } = {}) { + const usersSnap = await getDocs(collection(firestoreDB, "users")); + if (usersSnap.empty) return { scanned: 0, updated: 0 }; + + const docs = usersSnap.docs; + let updated = 0; + + if (dryRun) { + return { scanned: docs.length, updated: 0 }; + } + + for (let i = 0; i < docs.length; i += batchSize) { + const chunk = docs.slice(i, i + batchSize); + const batch = writeBatch(firestoreDB); + + chunk.forEach((d) => { + batch.update(d.ref, { totalTime: 0, points: 0 }); + }); + + await batch.commit(); + updated += chunk.length; + console.log(`✅ Restablecimiento en progreso: ${updated}/${docs.length}`); + } + + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log(`Reset complete. Se restablecieron totalTime y points para ${updated} usuarios.`); + return { scanned: docs.length, updated }; +} diff --git a/src/firebase/db/users.js b/src/firebase/db/users.js index 47455ea80..403f09dce 100644 --- a/src/firebase/db/users.js +++ b/src/firebase/db/users.js @@ -1,1034 +1 @@ -import { firestoreDB } from "../../main"; -import { getAuth } from 'firebase/auth'; -import { - doc, - collection, - query, - where, - setDoc, - getDoc, - getDocs, - updateDoc, - serverTimestamp, - deleteField, - increment, - getFirestore, -} from 'firebase/firestore'; -import { getUserProfilePicture } from "../img/users"; -import * as XLSX from 'xlsx'; -import { writeBatch } from "firebase/firestore"; - -const db = getFirestore(); - - -function getEmailUsername(email) { - var atIndex = email.indexOf('@'); - if (atIndex !== -1) { - return email.slice(0, atIndex); - } - return null; -} - -export async function createUser(userInfo) { - - userInfo.id = getEmailUsername(userInfo.email); - userInfo.uid = getEmailUsername(userInfo.email); - - userInfo.career = userInfo.major.id - userInfo.area = userInfo.major.school - - userInfo.name = userInfo.firstname.trim() + ' ' + userInfo.lastname.trim(); - - const userRef = doc(firestoreDB, "users", userInfo.uid); - return await setDoc(userRef, userInfo); -} - -export async function getUser(uid) { - const userRef = doc(firestoreDB, "users", uid); - const docSnap = await getDoc(userRef); - - if (docSnap.exists()) { - const data = docSnap.data() - const profilePictureUrl = await getUserProfilePicture(data.email); - return { ...data, profilePictureUrl }; - } else { - return null; - } -} - -export async function getCurrentUser() { - const auth = getAuth(); - if (auth.currentUser) { - const uid = getEmailUsername(auth.currentUser.email); - const user = await getUser(uid); - return user; - } - return null; -} - -// Función para obtener el día más cercano en la semana y la hora de inicio más temprana -export const getClosestDayAndStartTime = (schedules) => { - if (typeof schedules !== 'object' || schedules === null || Array.isArray(schedules)) { - console.error('Expected a map of schedules, but received:', schedules); - return { day: null, startTime: null }; - } - - const today = new Date().getDay(); // Día actual (0-6) donde 0 es domingo - const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; - - // Crear dos arrays, uno para los días futuros y otro para los pasados - const futureDays = daysOfWeek.slice(today); - const pastDays = daysOfWeek.slice(0, today); - - let closestDay = null; - let earliestStartTime = null; - - // Buscar primero entre los días futuros (desde hoy hasta el final de la semana) - futureDays.forEach(day => { - if (Array.isArray(schedules[day])) { - schedules[day].forEach(schedule => { - if (schedule.start) { - if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { - closestDay = day; - earliestStartTime = schedule.start; - } - } - }); - } - }); - - // Si no se encontró ningún día en el futuro, buscar en los días pasados (inicio de semana hasta hoy) - if (closestDay === null) { - pastDays.forEach(day => { - if (Array.isArray(schedules[day])) { - schedules[day].forEach(schedule => { - if (schedule.start) { - if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { - closestDay = day; - earliestStartTime = schedule.start; - } - } - }); - } - }); - } - - return { day: closestDay, startTime: earliestStartTime }; -}; - - -export async function getMaes() { - const usersRef = collection(firestoreDB, "users"); - const q = query(usersRef, where('role', 'in', ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi','tec'])); - - const querySnapshot = await getDocs(q); - - if (querySnapshot) { - let data = querySnapshot.docs.map(doc => doc.data()); - - // Filtrar usuarios que tienen un nombre - data = data.filter(item => item.name); - - // Obtener la URL de la foto de perfil - data = await Promise.all(data.map(async (item) => { - const profilePictureUrl = await getUserProfilePicture(item.email); - return { ...item, profilePictureUrl }; - })); - - // Obtener el día actual - const today = new Date().getDay(); // Día actual (0-6) - - // Ordenar por el día más cercano, la hora de inicio más temprana y alfabéticamente por nombre - data.sort((a, b) => { - // Obtener el día más cercano y la hora de inicio más temprana - const { day: dayA, startTime: startTimeA } = getClosestDayAndStartTime(a.weekSchedule); - const { day: dayB, startTime: startTimeB } = getClosestDayAndStartTime(b.weekSchedule); - - const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday','sunday']; - - // Crear un array cíclico desde el día actual - const daysOrdered = [...daysOfWeek.slice(today), ...daysOfWeek.slice(0, today)]; - - // Comparar días más cercanos, teniendo en cuenta el ciclo - const dayIndexA = daysOrdered.indexOf(dayA); - const dayIndexB = daysOrdered.indexOf(dayB); - const dayComparison = (dayIndexA === -1 ? 1 : (dayIndexB === -1 ? -1 : dayIndexA - dayIndexB)); - if (dayComparison !== 0) return dayComparison; - - // Comparar horas de inicio si los días son iguales - const startTimeComparison = (startTimeA === null ? 1 : (startTimeB === null ? -1 : startTimeA.localeCompare(startTimeB))); - if (startTimeComparison !== 0) return startTimeComparison; - - // Comparar alfabéticamente si ambos días y horas son iguales - return a.name.localeCompare(b.name); - }); - - return data; - } else { - return null; - } -} - -export async function getMaesNames() { - const usersRef = collection(firestoreDB, "users"); - const q = query(usersRef, where('role', 'in', ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi', 'tec'])); - - const querySnapshot = await getDocs(q); - - if (querySnapshot) { - let data = querySnapshot.docs.map(doc => doc.data()); - - // Filtrar usuarios que tienen un nombre - data = data.filter(item => item.name); - - // Obtener el día actual - const today = new Date().getDay(); // Día actual (0-6) - - // Ordenar por el día más cercano, la hora de inicio más temprana y alfabéticamente por nombre - data.sort((a, b) => { - // Obtener el día más cercano y la hora de inicio más temprana - const { day: dayA, startTime: startTimeA } = getClosestDayAndStartTime(a.weekSchedule); - const { day: dayB, startTime: startTimeB } = getClosestDayAndStartTime(b.weekSchedule); - - const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; - - // Crear un array cíclico desde el día actual - const daysOrdered = [...daysOfWeek.slice(today), ...daysOfWeek.slice(0, today)]; - - // Comparar días más cercanos, teniendo en cuenta el ciclo - const dayIndexA = daysOrdered.indexOf(dayA); - const dayIndexB = daysOrdered.indexOf(dayB); - const dayComparison = (dayIndexA === -1 ? 1 : (dayIndexB === -1 ? -1 : dayIndexA - dayIndexB)); - if (dayComparison !== 0) return dayComparison; - - // Comparar horas de inicio si los días son iguales - const startTimeComparison = (startTimeA === null ? 1 : (startTimeB === null ? -1 : startTimeA.localeCompare(startTimeB))); - if (startTimeComparison !== 0) return startTimeComparison; - - // Comparar alfabéticamente si ambos días y horas son iguales - return a.name.localeCompare(b.name); - }); - - return data; - } else { - return null; - } -} - - -export async function getUsersWithActiveSession(getProfilePicture = false) { - try { - // Get a reference to the users collection - const usersRef = collection(firestoreDB, "users"); - - // Use where clause to filter users with 'activeSession' object - const q = query(usersRef, where('activeSession', '!=', null)); - const querySnapshot = await getDocs(q); - - // Process the query results - if (querySnapshot) { - // Calculate the time 5 hours ago in seconds (18000 is 5hrs in seconds) - const fiveHoursAgoTimestampSeconds = Math.floor(Date.now() / 1000) - 18000; - - // Filter the docs before mapping to get profile pictures - const filteredDocs = querySnapshot.docs.filter(doc => { - const data = doc.data(); - return data.activeSession.startTime.seconds > fiveHoursAgoTimestampSeconds; - }); - - // Map the filtered docs to an array of promises - const usersPromises = filteredDocs.map(async (doc) => { - const data = doc.data(); - const profilePictureUrl = await getUserProfilePicture(data.email); - return { ...data, profilePictureUrl }; - }); - - // Wait for all promises to resolve and return the users - return Promise.all(usersPromises); - } else { - return null; - } - } catch (error) { - console.error('Error retrieving users:', error); - } -}; - -export async function updateUserInfo(userId, userInfo) { - userInfo['name'] = userInfo['firstname'].trim() + ' ' + userInfo['lastname'].trim() - const userRef = doc(firestoreDB, "users", userId); - return await updateDoc(userRef, userInfo); -} - -export async function updateUserSubjects(userId, newSubjects) { - const userRef = doc(firestoreDB, "users", userId); - return await updateDoc(userRef, { - subjects: newSubjects - }); -} - -export async function updateUserSchedule(userId, newSchedule) { - const userRef = doc(firestoreDB, "users", userId); - // Iterate over object keys - for (const day in newSchedule) { - // Check if the value is an empty array - if (Array.isArray(newSchedule[day]) && newSchedule[day].length === 0) { - // Delete the key with an empty array value - delete newSchedule[day]; - } - } - return await updateDoc(userRef, { - weekSchedule: newSchedule - }); -} - -export async function getTodaysMae() { - try { - // Step 1: Define the query to get users with a role different from 'user' - const usersRef = collection(firestoreDB, "users"); - const roleQuery = query(usersRef, where("role", "!=", "user")); - - // Step 2: Execute the role query - const querySnapshot = await getDocs(roleQuery); - - // Step 3: Get current day of the week - const daysOfWeek = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; - const currentDayIndex = new Date().getDay(); - const currentDay = daysOfWeek[currentDayIndex] - - - // Step 4: Filter results client-side to include only those with 'wednesday' in 'weekSchedule' - const users = []; - querySnapshot.forEach((doc) => { - const data = doc.data(); - // if (data.weekSchedule && data.weekSchedule["thursday"]) { - if (data.weekSchedule && data.weekSchedule[currentDay]) { - users.push(data); - } - }); - - // Step 5: Sort users by the earliest start time of the current day - users.sort((a, b) => { - - // TODO: Change to this when updating user schedule format - const aStartTime = a.weekSchedule[currentDay][0]?.start; - const bStartTime = b.weekSchedule[currentDay][0]?.start; - - // const bStartTime = b.weekSchedule[currentDay][0]?.start ?? `${Math.round((b.weekSchedule[currentDay][0]))}:00`; - // const aStartTime = a.weekSchedule[currentDay][0]?.start ?? `${Math.round((a.weekSchedule[currentDay][0]))}:00`; - - // Convert time strings to Date objects for comparison - const aTime = aStartTime ? new Date(`1970-01-01T${aStartTime}:00Z`) : new Date(); - const bTime = bStartTime ? new Date(`1970-01-01T${bStartTime}:00Z`) : new Date(); - - return aTime - bTime; - }); - - return users; - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -export async function startActiveSession(userId, userInfo, location) { - try { - const userRef = doc(firestoreDB, "users", userId); - return await updateDoc(userRef, { - activeSession: { - peerInfo: userInfo, - location, - status: 'PENDING', - startTime: serverTimestamp(), - } - }); - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -export async function stopActiveSession(userId) { - try { - const userRef = doc(firestoreDB, "users", userId); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - throw new Error("User not found"); - } - - // Gets start time from current Active Session - const userData = userDoc.data(); - const startTime = userData.activeSession?.startTime?.toDate(); - - if (!startTime) { - throw new Error("Active session start time not found"); - } - - // Calculates and adds the duration of the current session to the total time - const currentTime = new Date(); - const differenceInMinutes = Math.floor((currentTime - startTime) / (1000 * 60)); - - if (differenceInMinutes > 310) { - await updateDoc(userRef, { - activeSession: deleteField() - }); - return { timeLimitExceded: true, activeSessionDeleted: false, differenceInMinutes } - } - - const totalTime = (userData.totalTime || 0) + differenceInMinutes; - - // Updates the total time and stops current session - await updateDoc(userRef, { - totalTime: totalTime, - activeSession: deleteField() - }); - - return { totalTime, differenceInMinutes, activeSessionDeleted: true }; - } catch (error) { - return { activeSessionDeleted: false }; - } -} - -export async function incrementTotalTime(userId, time) { - const userRef = doc(firestoreDB, "users", userId); - - await updateDoc(userRef, { - totalTime: increment(time*60) - }); -} - - -export async function updateUserProfilePicture(userId, photoURL) { - try { - const userRef = doc(firestoreDB, 'users', userId); - - await updateDoc(userRef, { - photoURL: photoURL - }); - - } catch (error) { - console.error('Error updating user profile picture: ', error); - throw error; - } -} - -/** - * Clears the content of the weekSchedule field for users with specific roles (admin, coordi, mae), - * but keeps the field as an empty object. - * - * @returns {Promise} - A promise that resolves when all eligible weekSchedules are cleared. - */ -export async function clearAllUsersWeekSchedule() { - try { - - const usersRef = collection(firestoreDB, "users"); - - const querySnapshot = await getDocs(usersRef); - - - const eligibleRoles = ['admin', 'coordi', 'mae','tec','publi']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - weekSchedule: {} - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - - console.log("Week schedule content has been successfully cleared for eligible users."); - } catch (error) { - console.error("Error clearing weekSchedule content for eligible users: ", error); - throw error; - } -} - -export async function checkAndUpdateUserRole(file = null) { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - if (file) { - // Leer y procesar el archivo Excel acuerdate que empieza a contar desde 0 - const reader = new FileReader(); - reader.onload = async (event) => { - try { - const data = new Uint8Array(event.target.result); - const workbook = XLSX.read(data, { type: 'array' }); - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - // console.log(sheet) - const excelData = XLSX.utils.sheet_to_json(sheet, { header: 0 }); - console.log(excelData) - // Convertimos las matrículas del Excel a correos en formato lowercase@tec.mx - const emailsFromExcel = excelData - .map(row => row["Matrícula"]?.toLowerCase() + "@tec.mx") - - console.log(emailsFromExcel); - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - const eligibleRoles = ['mae', 'coordi', 'publi', 'tec', 'admin']; - - const userEmail = userData.email?.toLowerCase(); - const userMatricula = userData.matricula?.toLowerCase(); - - if (eligibleRoles.includes(userData.role)) { - if (!emailsFromExcel.includes(userEmail)) { - return updateDoc(userRef, { role: "exmae" }); - } - } else { - // Si el usuario no tiene un rol elegible, se le asigna "mae" con estado "becario" - if (emailsFromExcel.includes(userEmail)) { - return updateUserToMae({ - matricula: userMatricula, - role: "mae", - status: "becario", - point: 0, - useCoins: 0, - }); - } - } - - return Promise.resolve(); - }); - - await Promise.all(promises); - //console.log("Roles actualizados con base en el archivo Excel."); - } catch (error) { - console.error("Error al procesar el archivo Excel:", error); - throw error; - } - }; - - reader.readAsArrayBuffer(file); - } else { - // Si no hay archivo, ejecutamos la lógica normal - const eligibleRoles = ['mae', 'coordi', 'publi','tec']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - if (eligibleRoles.includes(userData.role)) { - const isWeekScheduleEmpty = Object.values(userData.weekSchedule).every(day => day.length === 0); - const isTotalTimeEquals0 = userData.totalTime == 0; - const hasNoSubjects = !userData.subjects || userData.subjects.length === 0; - if (isWeekScheduleEmpty && isTotalTimeEquals0 && hasNoSubjects) { - return updateDoc(userRef, { role: "exmae" }); - } - } - - return Promise.resolve(); - }); - - await Promise.all(promises); - console.log("Roles actualizados con base en weekSchedule, totalTime, y subjects."); - } - } catch (error) { - console.error("Error actualizando roles de los usuarios: ", error); - throw error; - } -} - -export async function updateUserToMae(data) { - const { role, matricula, status } = data; - const badges = [ - { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, - { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, - { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, - { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, - { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, - { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, - { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, - { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, - { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, - { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, - { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, - { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, - { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, - { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, - { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, - { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, - { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, - { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } - ]; - - - - const background = [ - { "id": "1", "image_url": "/assets/back/1.svg", "bought": true, "price": 0 }, - { "id": "2", "image_url": "/assets/back/2.svg", "bought": false, "price": 25 }, - { "id": "3", "image_url": "/assets/back/3.svg", "bought": false, "price": 25}, - { "id": "4", "image_url": "/assets/back/4.svg", "bought": false, "price": 25 }, - { "id": "5", "image_url": "/assets/back/5.svg", "bought": false, "price": 50 }, - { "id": "6", "image_url": "/assets/back/6.svg", "bought": false, "price": 50 }, - { "id": "7", "image_url": "/assets/back/7.svg", "bought": false, "price": 75 }, - { "id": "8", "image_url": "/assets/back/8.svg", "bought": false, "price": 100 }, - ]; - - if (!role || !matricula || !status) { - throw new Error("role, matricula, and status are required fields."); - } - - try { - const usersRef = collection(firestoreDB, "users"); - const userQuery = query(usersRef, where("email", "==", `${matricula.toLowerCase()}@tec.mx`)); - const querySnapshot = await getDocs(userQuery); - - if (querySnapshot.empty) { - console.log("No user found with the given matricula."); - return; - } - - // Procesar cada usuario encontrado - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (userData.role === 'user' || userData.status === 'estudiante') { - return updateDoc(userRef, { - role: role.value, - status: status.value, - weekSchedule: {}, - subjects: [], - totalTime: 0, - badges: badges, - points: 0, - useCoins: 0, - background: background, - }); - } else { - - return updateDoc(userRef, { - role: role.value, - status: status.value - }); - } - }); - - await Promise.all(promises); - console.log("Usuarios actualizados exitosamente."); - } catch (error) { - console.error("Error al actualizar los usuarios: ", error); - } -} - -export const saveScheduleSubjectsExperience = async () => { - try { - const usersRef = collection(db, 'users'); - const usersSnap = await getDocs(usersRef); - - if (usersSnap.empty) { - console.error("No se encontraron usuarios en la tabla 'users'."); - return; - } - - const rolesPermitidos = ['admin', 'publi', 'mae', 'coordi', 'tec']; - - const updatePromises = []; - usersSnap.forEach(async (userDoc) => { - const user = userDoc.data(); - - if (!rolesPermitidos.includes(user.role)) { - return; - } - - let puntos = 0; - - if (user.subjects && user.subjects.length > 0) { - puntos += 15; - } else { - puntos -= 30; - await updateUserAchievementBadge(user.uid, "18"); - } - - if (user.weekSchedule && Object.keys(user.weekSchedule).length > 0) { - puntos += 100; - } else { - puntos -= 500; - await updateUserAchievementBadge(user.uid, "18"); - } - - const userRef = doc(db, 'users', userDoc.id); - updatePromises.push( - updateDoc(userRef, { - points: (user.points || 0) + puntos - }) - ); - }); - - await Promise.all(updatePromises); - } catch (error) { - console.error("Error al guardar la experiencia:", error); - } -}; - - -export async function updatePoints(uid, newPoints) { - const usersRef = collection(db, 'users'); - const usersSnap = await getDocs(usersRef); - const users = usersSnap.docs.map(doc => ({ id: doc.id, ...doc.data() })); - const user = users.find(user => user.uid === uid); - - if (user) { - const userRef = doc(db, 'users', user.id); - const updatedPoints = (user.points || 0) + newPoints; - await updateDoc(userRef, { points: updatedPoints }); - //console.log(`Puntos actualizados para ${user.name}: ${updatedPoints}`); - user.points = updatedPoints; - if (newPoints < 0){ - await updateUserAchievementBadge(uid, "18") - } - } else { - console.log(`Usuario con uid ${uid} no encontrado.`); - } - - return users; -} - -export async function getExperience() { - const usersRef = collection(firestoreDB, "users"); - const q = query(usersRef, where('role', 'in', ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi','tec'])); - - const querySnapshot = await getDocs(q); - - if (querySnapshot) { - let data = querySnapshot.docs.map(doc => doc.data()); - - // Filtrar usuarios que tienen un nombre - data = data.filter(item => item.name); - - // Ordenar por puntos de mayor a menor - data.sort((a, b) => b.points - a.points); - - return data; - } else { - return null; - } -} - -// Funcion especial si mas adelante quieren agregar logros -export async function addBadgesToEligibleUsers() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const badges = [ - { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, - { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, - { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, - { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, - { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, - { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, - { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, - { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, - { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, - { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, - { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, - { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, - { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, - { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, - { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, - { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, - { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, - { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } - ]; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - badges: badges - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - - console.log("Badges have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding badges to eligible users: ", error); - throw error; - } -} - - -// actualizar le achieved del usuario -export async function updateUserAchievementBadge(uid, badgeId) { - try { - - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - console.error("Usuario no encontrado"); - return; - } - - const userData = userDoc.data(); - const badges = userData.badges || []; - - const updatedBadges = badges.map((badge) => { - if (badge.id === badgeId) { - return { ...badge, achieved: true }; - } - return badge; - }); - - await updateDoc(userRef, { - badges: updatedBadges - }); - - // console.log(`El logro con id ${badgeId} se ha actualizado correctamente para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el logro del usuario:", error); - throw error; - } -} - - -// Contador de badges -export async function countAchievedBadges(uid) { - try { - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - console.error("Usuario no encontrado"); - return 0; - } - - const badges = userDoc.data().badges || []; - - const achievedCount = badges.reduce((count, badge) => { - return count + (badge.achieved ? 1 : 0); - }, 0); - - return achievedCount; - } catch (error) { - console.error("Error al contar los logros alcanzados:", error); - throw error; - } -} - -// Añadir nuevos backgrounds a los usuarios sin borrar los existentes -export async function addBackgroundUsers() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const newBackgrounds = [ - { id: '8', image_url: '/assets/back/8.svg', bought: false, price: 100 }, - // Aquí puedes agregar más fondos nuevos... - ]; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - const currentBackgrounds = userData.background || []; - - // Crear un mapa de fondos existentes (para evitar duplicados) - const backgroundMap = new Map(currentBackgrounds.map(bg => [bg.id, bg])); - - // Añadir los nuevos fondos solo si no existen ya - newBackgrounds.forEach(bg => { - if (!backgroundMap.has(bg.id)) { - backgroundMap.set(bg.id, bg); - } - }); - - const mergedBackgrounds = Array.from(backgroundMap.values()); - - return updateDoc(userRef, { - background: mergedBackgrounds, - myBackground: userData.myBackground || "/assets/back/1.svg", // conservar el que ya tiene - useCoins: userData.useCoins || 0, // conservar las monedas que ya tiene - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - - console.log("Backgrounds have been merged successfully for eligible users."); - } catch (error) { - console.error("Error merging backgrounds for eligible users: ", error); - throw error; -} -} - - - -// actualizar le achieved del usuario -export async function updateUserBackground(uid, backId, coins, userCoins) { - try { - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - console.error("Usuario no encontrado"); - return; - } - - const userData = userDoc.data(); - const background = userData.background || []; - - const updatedBackground = background.map((back) => { - if (back.id === backId) { - return { ...back, bought: true }; - } - return back; - }); - - await updateDoc(userRef, { - background: updatedBackground, - useCoins: userCoins + coins - }); - - console.log(`El fondo con id ${backId} se ha actualizado correctamente para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el fondo del usuario:", error); - throw error; - } -} - -// Actualizar fondo -export async function updateUserBackgroundImage(uid, backgroundUrl) { - try { - const userRef = doc(firestoreDB, "users", uid); - await updateDoc(userRef, { - myBackground: backgroundUrl - }); - console.log(`El fondo se ha actualizado a ${backgroundUrl} para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el fondo del usuario:", error); - throw error; - } -} - - -export async function getTotalMaes() { - try { - const usersRef = collection(firestoreDB, "users"); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const q = query(usersRef, where("role", "in", eligibleRoles)); - - const querySnapshot = await getDocs(q); - - return querySnapshot.size -1 ; - } catch (error) { - console.error("Error al obtener el total de MAEs: ", error); - throw error; - } -} - - - -// Añadir nuevas variables -export async function addExtraVariables() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - asesoriasGrupales: 0, - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - - console.log("Background have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding background to eligible users: ", error); - throw error; - } -} - - - -export async function clearUsersData() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "users")); - const rolesToUpdate = ["admin", "coordi", "mae", "tec", "publi"]; - - const updatePromises = []; - - querySnapshot.forEach(docSnapshot => { - const userData = docSnapshot.data(); - if (rolesToUpdate.includes(userData.role)) { - const userRef = doc(firestoreDB, "users", docSnapshot.id); - updatePromises.push(updateDoc(userRef, { - useCoins: userData.useCoins - userData.points, - subjects: [], - totalTime: 0, - points: 0 - })); - } - }); - - await Promise.all(updatePromises); - console.log("Usuarios actualizados correctamente."); - } catch (error) { - console.error("Error al actualizar usuarios: ", error); - throw error; - } -} - - -export async function resetAllUsersTotalTimeAndPoints({ dryRun = false, batchSize = 450 } = {}) { - const usersSnap = await getDocs(collection(firestoreDB, "users")); - if (usersSnap.empty) return { scanned: 0, updated: 0 }; - - const docs = usersSnap.docs; - let updated = 0; - - if (dryRun) { - return { scanned: docs.length, updated: 0 }; - } - - for (let i = 0; i < docs.length; i += batchSize) { - const chunk = docs.slice(i, i + batchSize); - const batch = writeBatch(firestoreDB); - - chunk.forEach((d) => { - batch.update(d.ref, { totalTime: 0, points: 0 }); - }); - - await batch.commit(); - updated += chunk.length; - console.log(`✅ Restablecimiento en progreso: ${updated}/${docs.length}`); - } - - console.log(`🎉 Listo. Se restablecieron totalTime y points para ${updated} usuarios.`); - return { scanned: docs.length, updated }; -} +export * from './users.cached'; diff --git a/src/firebase/img/users.js b/src/firebase/img/users.js index 7b6ac3d24..38a5bf74a 100644 --- a/src/firebase/img/users.js +++ b/src/firebase/img/users.js @@ -1,16 +1,35 @@ import { ref, getDownloadURL, getStorage, uploadBytes } from "firebase/storage"; -import { firebaseStorage} from "../../main"; -import { v4 } from "uuid"; import { initializeApp } from "firebase/app"; +import { firebaseStorage } from "../../main"; +import { invalidateCacheTags, withCache } from "../cache/cache"; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys, profilePictureTag } from "../cache/config"; + +const DEFAULT_PROFILE_PICTURE = 'https://randomuser.me/api/portraits/lego/5.jpg'; + export const getUserProfilePicture = async (email) => { - try { - const url = await getDownloadURL(ref(firebaseStorage, `users/${email}/photo`)); - return url; - } catch (error) { - console.error(email, 'Has no profile picture') - return 'https://randomuser.me/api/portraits/lego/5.jpg'; + if (!email) { + return DEFAULT_PROFILE_PICTURE; } -} + + const normalizedEmail = email.toLowerCase(); + + return await withCache( + cacheKeys.profilePicture(normalizedEmail), + { + ttlMs: CACHE_TTL_MS.PROFILE_PICTURE, + persist: true, + tags: [CACHE_TAGS.PROFILE_PICTURES, profilePictureTag(normalizedEmail)] + }, + async () => { + try { + return await getDownloadURL(ref(firebaseStorage, `users/${normalizedEmail}/photo`)); + } catch (error) { + console.error(normalizedEmail, 'Has no profile picture'); + return DEFAULT_PROFILE_PICTURE; + } + } + ); +}; const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, @@ -25,26 +44,14 @@ const firebaseConfig = { export const firebaseAppImage = initializeApp(firebaseConfig); -export const storage = getStorage(firebaseAppImage) +export const storage = getStorage(firebaseAppImage); -/** - * Subir un archivo a Firebase Storage y obtener su URL de descarga. - * - * @param {File} file - El archivo a subir. - * @param {string} email - El email del usuario que determina la ruta de almacenamiento. - * @returns {Promise} La URL de descarga del archivo subido. - */ export async function uploadFile(file, email) { try { - // Crear una referencia al archivo en la ruta específica const storageRef = ref(storage, `users/${email}/photo`); - - // Subir el archivo await uploadBytes(storageRef, file); - - // Obtener la URL de descarga const url = await getDownloadURL(storageRef); - + await invalidateUserProfilePictureCache(email); return url; } catch (error) { console.error('Error uploading file:', error); @@ -52,21 +59,21 @@ export async function uploadFile(file, email) { } } -/** - * Subir un archivo a Firebase Storage y obtener su URL de descarga. - * - * @param {File} file - El archivo a subir. - * @param {string} path - La ruta en Storage donde se almacenará el archivo. - * @returns {Promise} La URL de descarga del archivo subido. - */ export async function addAnnoucement(file, path) { try { const storageRef = ref(storage, path); await uploadBytes(storageRef, file); - const url = await getDownloadURL(storageRef); - return url; + return await getDownloadURL(storageRef); } catch (error) { console.error('Error uploading file:', error); throw error; } } + +export async function invalidateUserProfilePictureCache(email) { + if (!email) { + return; + } + + await invalidateCacheTags([profilePictureTag(email.toLowerCase())]); +} diff --git a/src/views/AdminAsesorias.vue b/src/views/AdminAsesorias.vue index eead37daa..965a2c395 100644 --- a/src/views/AdminAsesorias.vue +++ b/src/views/AdminAsesorias.vue @@ -227,4 +227,4 @@ onMounted(() => { border-top-left-radius: 20px; } - \ No newline at end of file + diff --git a/src/views/AdminFunciones.vue b/src/views/AdminFunciones.vue index 480cf3572..a553bddb0 100644 --- a/src/views/AdminFunciones.vue +++ b/src/views/AdminFunciones.vue @@ -15,7 +15,7 @@ import { clearUsersData, resetAllUsersTotalTimeAndPoints } from '../firebase/db/users'; -import { deleteOldAsesorias} from '../firebase/db/asesorias.js' +import { deleteOldAsesorias} from '../firebase/db/asesorias' const toast = useToast(); const confirm = useConfirm(); diff --git a/src/views/pages/Landing.vue b/src/views/pages/Landing.vue index eb0b98229..5bd8ee278 100644 --- a/src/views/pages/Landing.vue +++ b/src/views/pages/Landing.vue @@ -419,4 +419,4 @@ const goToAsesoria = async (asesoria) => { color: inherit; } - \ No newline at end of file + From 42318ef0ed46d351ab0f68202ed67ec70d0dd04b Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:03:22 -0600 Subject: [PATCH 2/8] small fixes to cache logic --- src/firebase/cache/cache.js | 36 +++++++++++++++++++-------- src/firebase/cache/config.js | 1 + src/firebase/db/annoucement.cached.js | 8 ++++-- src/firebase/db/users.cached.js | 6 ----- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/firebase/cache/cache.js b/src/firebase/cache/cache.js index cc4d6929a..523eeaf45 100644 --- a/src/firebase/cache/cache.js +++ b/src/firebase/cache/cache.js @@ -240,7 +240,11 @@ export async function setCachedValue(key, value, { ttlMs = 0, tags = [], persist setMemoryEntry(key, entry); if (persist) { - await writePersistentEntry(entry); + try { + await writePersistentEntry(entry); + } catch (error) { + console.warn(`Persistent cache write failed for ${key}; using memory cache only.`, error); + } } return cloneValue(entry.value); @@ -258,9 +262,13 @@ export async function withCache( } if (persist) { - const persistentEntry = await hydratePersistentEntry(key); - if (persistentEntry) { - return cloneValue(persistentEntry.value); + try { + const persistentEntry = await hydratePersistentEntry(key); + if (persistentEntry) { + return cloneValue(persistentEntry.value); + } + } catch (error) { + console.warn(`Persistent cache read failed for ${key}; loading fresh data.`, error); } } } @@ -285,7 +293,11 @@ export async function withCache( export async function invalidateCacheKey(key) { memoryCache.delete(key); - await deletePersistentEntry(key); + try { + await deletePersistentEntry(key); + } catch (error) { + console.warn(`Persistent cache delete failed for ${key}.`, error); + } } export async function invalidateCacheTags(tags = []) { @@ -300,12 +312,16 @@ export async function invalidateCacheTags(tags = []) { } } - const persistentEntries = await getAllPersistentEntries(); - const keysToDelete = persistentEntries - .filter((entry) => entry.tags?.some((tag) => wantedTags.includes(tag))) - .map((entry) => entry.key); + try { + const persistentEntries = await getAllPersistentEntries(); + const keysToDelete = persistentEntries + .filter((entry) => entry.tags?.some((tag) => wantedTags.includes(tag))) + .map((entry) => entry.key); - await Promise.all(keysToDelete.map((key) => deletePersistentEntry(key))); + await Promise.all(keysToDelete.map((key) => deletePersistentEntry(key))); + } catch (error) { + console.warn('Persistent cache tag invalidation failed.', error); + } } export function clearMemoryCache() { diff --git a/src/firebase/cache/config.js b/src/firebase/cache/config.js index 322578b8e..90c4f0349 100644 --- a/src/firebase/cache/config.js +++ b/src/firebase/cache/config.js @@ -57,6 +57,7 @@ export const cacheKeys = { announcementsVisible: () => 'announcements:visible', announcementsEdit: () => 'announcements:edit', announcementsGroup: () => 'announcements:group', + announcementsAllGroup: () => 'announcements:group:all', attendanceToday: (dateString) => `attendance:today:${dateString}`, attendanceByDate: (dateString) => `attendance:date:${dateString}`, attendanceStudent: (uid, dateString) => `attendance:student:${uid}:${dateString}`, diff --git a/src/firebase/db/annoucement.cached.js b/src/firebase/db/annoucement.cached.js index 2b32ebac4..0c4e29289 100644 --- a/src/firebase/db/annoucement.cached.js +++ b/src/firebase/db/annoucement.cached.js @@ -66,11 +66,15 @@ export async function updateUserAsistence(announcementId, userId) { return result; } -export const addExtraVariables = announcementDb.addExtraVariables; +export async function addExtraVariables() { + const result = await announcementDb.addExtraVariables(); + await invalidateAnnouncementCaches(); + return result; +} export async function getAnnouncementsAllGrupales(options = {}) { return await withCache( - 'announcements:group:all', + cacheKeys.announcementsAllGroup(), { ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, persist: true, diff --git a/src/firebase/db/users.cached.js b/src/firebase/db/users.cached.js index c1bf6255c..0322fd213 100644 --- a/src/firebase/db/users.cached.js +++ b/src/firebase/db/users.cached.js @@ -92,12 +92,6 @@ async function getMaeDirectory(options = {}) { ); } -function getSemesterCacheKey() { - const now = new Date(); - const semester = now.getMonth() < 6 ? '01' : '02'; - return `${now.getFullYear()}-${semester}`; -} - async function invalidateUserCaches(userId, { includeActive = false, includeLeaderboard = false } = {}) { const tags = [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, CACHE_TAGS.CURRENT_USER, CACHE_TAGS.MAES]; From 53fb47707dd9ad82e623f9a8b3c35d2b0d6853c9 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:11:37 -0600 Subject: [PATCH 3/8] collapse .cached files --- src/firebase/db/annoucement.cached.js | 104 --- src/firebase/db/annoucement.js | 105 ++- src/firebase/db/asesorias.cached.js | 203 ----- src/firebase/db/asesorias.js | 204 ++++- src/firebase/db/attendance.cached.js | 99 --- src/firebase/db/attendance.js | 100 ++- src/firebase/db/campuses.cached.js | 29 - src/firebase/db/campuses.js | 30 +- src/firebase/db/maeteca.cached.js | 419 ---------- src/firebase/db/maeteca.js | 420 +++++++++- src/firebase/db/majors.cached.js | 27 - src/firebase/db/majors.js | 28 +- src/firebase/db/subjects.cached.js | 45 -- src/firebase/db/subjects.js | 46 +- src/firebase/db/users.cached.js | 1032 ------------------------ src/firebase/db/users.js | 1033 ++++++++++++++++++++++++- 16 files changed, 1958 insertions(+), 1966 deletions(-) delete mode 100644 src/firebase/db/annoucement.cached.js delete mode 100644 src/firebase/db/asesorias.cached.js delete mode 100644 src/firebase/db/attendance.cached.js delete mode 100644 src/firebase/db/campuses.cached.js delete mode 100644 src/firebase/db/maeteca.cached.js delete mode 100644 src/firebase/db/majors.cached.js delete mode 100644 src/firebase/db/subjects.cached.js delete mode 100644 src/firebase/db/users.cached.js diff --git a/src/firebase/db/annoucement.cached.js b/src/firebase/db/annoucement.cached.js deleted file mode 100644 index 0c4e29289..000000000 --- a/src/firebase/db/annoucement.cached.js +++ /dev/null @@ -1,104 +0,0 @@ -import * as announcementDb from './annoucement.legacy'; -import { invalidateCacheTags, withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -async function invalidateAnnouncementCaches() { - await invalidateCacheTags([CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS]); -} - -export async function saveAnnouncement(announcementData, selectedFile) { - const result = await announcementDb.saveAnnouncement(announcementData, selectedFile); - await invalidateAnnouncementCaches(); - return result; -} - -export async function getAnnouncementsEdit(options = {}) { - return await withCache( - cacheKeys.announcementsEdit(), - { - ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS_EDIT, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ANNOUNCEMENTS] - }, - async () => await announcementDb.getAnnouncementsEdit() - ); -} - -export async function getAnnouncements(options = {}) { - return await withCache( - cacheKeys.announcementsVisible(), - { - ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ANNOUNCEMENTS] - }, - async () => await announcementDb.getAnnouncements() - ); -} - -export async function getAnnouncementsGrupales(options = {}) { - return await withCache( - cacheKeys.announcementsGroup(), - { - ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] - }, - async () => await announcementDb.getAnnouncementsGrupales() - ); -} - -export async function addUserToPreregsiter(announcementId, user) { - const result = await announcementDb.addUserToPreregsiter(announcementId, user); - await invalidateAnnouncementCaches(); - return result; -} - -export const processAsistence = announcementDb.processAsistence; -export const processConfirms = announcementDb.processConfirms; - -export async function updateUserAsistence(announcementId, userId) { - const result = await announcementDb.updateUserAsistence(announcementId, userId); - await invalidateAnnouncementCaches(); - return result; -} - -export async function addExtraVariables() { - const result = await announcementDb.addExtraVariables(); - await invalidateAnnouncementCaches(); - return result; -} - -export async function getAnnouncementsAllGrupales(options = {}) { - return await withCache( - cacheKeys.announcementsAllGroup(), - { - ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] - }, - async () => await announcementDb.getAnnouncementsAllGrupales() - ); -} - -export async function deleteAnnouncementById(id) { - const result = await announcementDb.deleteAnnouncementById(id); - await invalidateAnnouncementCaches(); - return result; -} - -export async function updateAnnouncement(announcementId, updatedData) { - const result = await announcementDb.updateAnnouncement(announcementId, updatedData); - await invalidateAnnouncementCaches(); - return result; -} - -export async function toggleVisibilityById(id) { - const result = await announcementDb.toggleVisibilityById(id); - await invalidateAnnouncementCaches(); - return result; -} diff --git a/src/firebase/db/annoucement.js b/src/firebase/db/annoucement.js index aa5d17894..0c4e29289 100644 --- a/src/firebase/db/annoucement.js +++ b/src/firebase/db/annoucement.js @@ -1 +1,104 @@ -export * from './annoucement.cached'; +import * as announcementDb from './annoucement.legacy'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +async function invalidateAnnouncementCaches() { + await invalidateCacheTags([CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS]); +} + +export async function saveAnnouncement(announcementData, selectedFile) { + const result = await announcementDb.saveAnnouncement(announcementData, selectedFile); + await invalidateAnnouncementCaches(); + return result; +} + +export async function getAnnouncementsEdit(options = {}) { + return await withCache( + cacheKeys.announcementsEdit(), + { + ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS_EDIT, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsEdit() + ); +} + +export async function getAnnouncements(options = {}) { + return await withCache( + cacheKeys.announcementsVisible(), + { + ttlMs: CACHE_TTL_MS.ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncements() + ); +} + +export async function getAnnouncementsGrupales(options = {}) { + return await withCache( + cacheKeys.announcementsGroup(), + { + ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsGrupales() + ); +} + +export async function addUserToPreregsiter(announcementId, user) { + const result = await announcementDb.addUserToPreregsiter(announcementId, user); + await invalidateAnnouncementCaches(); + return result; +} + +export const processAsistence = announcementDb.processAsistence; +export const processConfirms = announcementDb.processConfirms; + +export async function updateUserAsistence(announcementId, userId) { + const result = await announcementDb.updateUserAsistence(announcementId, userId); + await invalidateAnnouncementCaches(); + return result; +} + +export async function addExtraVariables() { + const result = await announcementDb.addExtraVariables(); + await invalidateAnnouncementCaches(); + return result; +} + +export async function getAnnouncementsAllGrupales(options = {}) { + return await withCache( + cacheKeys.announcementsAllGroup(), + { + ttlMs: CACHE_TTL_MS.GROUP_ANNOUNCEMENTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] + }, + async () => await announcementDb.getAnnouncementsAllGrupales() + ); +} + +export async function deleteAnnouncementById(id) { + const result = await announcementDb.deleteAnnouncementById(id); + await invalidateAnnouncementCaches(); + return result; +} + +export async function updateAnnouncement(announcementId, updatedData) { + const result = await announcementDb.updateAnnouncement(announcementId, updatedData); + await invalidateAnnouncementCaches(); + return result; +} + +export async function toggleVisibilityById(id) { + const result = await announcementDb.toggleVisibilityById(id); + await invalidateAnnouncementCaches(); + return result; +} diff --git a/src/firebase/db/asesorias.cached.js b/src/firebase/db/asesorias.cached.js deleted file mode 100644 index 2d098dc6f..000000000 --- a/src/firebase/db/asesorias.cached.js +++ /dev/null @@ -1,203 +0,0 @@ -import * as asesoriaDb from './asesorias.legacy'; -import { invalidateCacheTags, withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -const SEMESTER_START = new Date('2024-08-05'); - -function normalizeDateKey(date) { - if (!date) { - return 'all'; - } - - if (date instanceof Date) { - return date.toISOString(); - } - - return String(date); -} - -function getCurrentSemesterRange() { - const now = new Date(); - const currentYear = now.getFullYear(); - - if (now.getMonth() < 6) { - return { - start: new Date(currentYear, 0, 1), - end: new Date(currentYear, 5, 30, 23, 59, 59, 999) - }; - } - - return { - start: new Date(currentYear, 6, 1), - end: new Date(currentYear, 11, 31, 23, 59, 59, 999) - }; -} - -async function invalidateAsesoriaCaches() { - await invalidateCacheTags([CACHE_TAGS.ASESORIAS]); -} - -function timestampToMs(value) { - if (!value) return null; - if (typeof value?.toMillis === 'function') return value.toMillis(); - if (typeof value?.toDate === 'function') return value.toDate().getTime(); - if (value instanceof Date) return value.getTime(); - if (typeof value === 'string') return new Date(value).getTime(); - return null; -} - -export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { - const result = await asesoriaDb.addAsesoria(maeInfo, userInfo, subject, comment, rating); - await invalidateAsesoriaCaches(); - return result; -} - -export async function getAsesorias(startDate = null, endDate = null, options = {}) { - const startKey = normalizeDateKey(startDate); - const endKey = normalizeDateKey(endDate); - - return await withCache( - cacheKeys.asesoriasRange(startKey, endKey), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => await asesoriaDb.getAsesorias(startDate, endDate) - ); -} - -export async function getAsesoriasCountForUserInCurrentSemester(userId, options = {}) { - const { start, end } = getCurrentSemesterRange(); - const semesterKey = `${start.getFullYear()}-${start.getMonth() < 6 ? '01' : '02'}`; - - return await withCache( - cacheKeys.asesoriasSemesterByPeer(userId, semesterKey), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => { - const asesorias = await getAsesorias(start, end, options); - return (asesorias ?? []).filter((doc) => { - const dateMs = timestampToMs(doc.date); - return ( - doc.peerInfo?.uid === userId && - doc.duplicate !== true && - dateMs !== null && - dateMs >= start.getTime() && - dateMs <= end.getTime() - ); - }).length; - } - ); -} - -export async function getAsesoriasByUid(uid, options = {}) { - const today = new Date(); - const asesorias = await getAsesorias(SEMESTER_START, today, options); - return (asesorias ?? []).filter((asesoria) => asesoria.peerInfo?.uid === uid); -} - -export async function updateAllExperienceAsesorias() { - const result = await asesoriaDb.updateAllExperienceAsesorias(); - await invalidateAsesoriaCaches(); - return result; -} - -export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { - const result = await asesoriaDb.updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate); - await invalidateAsesoriaCaches(); - return result; -} - -export async function getCommentsByUid(uid, options = {}) { - const asesorias = await getAsesoriasByUid(uid, options); - return (asesorias ?? []).filter((asesoria) => asesoria.comment?.trim()); -} - -export async function getAsesoriasByUidAndRating(uidUser, uidPeer = null, options = {}) { - return await withCache( - cacheKeys.asesoriasPendingRating(uidUser, uidPeer ?? 'all'), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => await asesoriaDb.getAsesoriasByUidAndRating(uidUser, uidPeer) - ); -} - -export async function updateAsesoria(id, data) { - const result = await asesoriaDb.updateAsesoria(id, data); - await invalidateAsesoriaCaches(); - return result; -} - -export async function getTotalAsesorias(startDate = null, endDate = null, options = {}) { - const asesorias = await getAsesorias(startDate, endDate, options); - return (asesorias ?? []).length; -} - -export async function getAsesoriasCountByUser(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const userAsesoriasSet = new Set((asesorias ?? []).map(doc => doc.userInfo?.uid).filter(Boolean)); - return userAsesoriasSet.size; -} - -export async function getAsesoriasCountByArea(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const areasCount = {}; - - (asesorias ?? []).forEach((asesoria) => { - const subjectArea = asesoria?.subject?.area; - const userUid = asesoria?.userInfo?.uid; - - if (!subjectArea || !userUid) { - return; - } - - if (!areasCount[subjectArea]) { - areasCount[subjectArea] = { - totalAsesorias: 0, - userUids: new Set() - }; - } - - areasCount[subjectArea].totalAsesorias++; - areasCount[subjectArea].userUids.add(userUid); - }); - - return Object.keys(areasCount).map(area => ({ - area, - totalAsesorias: areasCount[area].totalAsesorias, - totalUniqueUsers: areasCount[area].userUids.size - })); -} - -export async function getAsesoriasCountByCampus(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const campusCount = {}; - - (asesorias ?? []).forEach((asesoria) => { - const campus = asesoria?.userInfo?.campus; - if (campus) { - campusCount[campus] = (campusCount[campus] || 0) + 1; - } - }); - - return Object.keys(campusCount).map(campus => ({ - campus, - totalAsesorias: campusCount[campus] - })); -} - -export async function deleteOldAsesorias() { - const result = await asesoriaDb.deleteOldAsesorias(); - await invalidateAsesoriaCaches(); - return result; -} diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 84b1fff34..2d098dc6f 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -1 +1,203 @@ -export * from './asesorias.cached'; +import * as asesoriaDb from './asesorias.legacy'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +const SEMESTER_START = new Date('2024-08-05'); + +function normalizeDateKey(date) { + if (!date) { + return 'all'; + } + + if (date instanceof Date) { + return date.toISOString(); + } + + return String(date); +} + +function getCurrentSemesterRange() { + const now = new Date(); + const currentYear = now.getFullYear(); + + if (now.getMonth() < 6) { + return { + start: new Date(currentYear, 0, 1), + end: new Date(currentYear, 5, 30, 23, 59, 59, 999) + }; + } + + return { + start: new Date(currentYear, 6, 1), + end: new Date(currentYear, 11, 31, 23, 59, 59, 999) + }; +} + +async function invalidateAsesoriaCaches() { + await invalidateCacheTags([CACHE_TAGS.ASESORIAS]); +} + +function timestampToMs(value) { + if (!value) return null; + if (typeof value?.toMillis === 'function') return value.toMillis(); + if (typeof value?.toDate === 'function') return value.toDate().getTime(); + if (value instanceof Date) return value.getTime(); + if (typeof value === 'string') return new Date(value).getTime(); + return null; +} + +export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { + const result = await asesoriaDb.addAsesoria(maeInfo, userInfo, subject, comment, rating); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getAsesorias(startDate = null, endDate = null, options = {}) { + const startKey = normalizeDateKey(startDate); + const endKey = normalizeDateKey(endDate); + + return await withCache( + cacheKeys.asesoriasRange(startKey, endKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await asesoriaDb.getAsesorias(startDate, endDate) + ); +} + +export async function getAsesoriasCountForUserInCurrentSemester(userId, options = {}) { + const { start, end } = getCurrentSemesterRange(); + const semesterKey = `${start.getFullYear()}-${start.getMonth() < 6 ? '01' : '02'}`; + + return await withCache( + cacheKeys.asesoriasSemesterByPeer(userId, semesterKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => { + const asesorias = await getAsesorias(start, end, options); + return (asesorias ?? []).filter((doc) => { + const dateMs = timestampToMs(doc.date); + return ( + doc.peerInfo?.uid === userId && + doc.duplicate !== true && + dateMs !== null && + dateMs >= start.getTime() && + dateMs <= end.getTime() + ); + }).length; + } + ); +} + +export async function getAsesoriasByUid(uid, options = {}) { + const today = new Date(); + const asesorias = await getAsesorias(SEMESTER_START, today, options); + return (asesorias ?? []).filter((asesoria) => asesoria.peerInfo?.uid === uid); +} + +export async function updateAllExperienceAsesorias() { + const result = await asesoriaDb.updateAllExperienceAsesorias(); + await invalidateAsesoriaCaches(); + return result; +} + +export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { + const result = await asesoriaDb.updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getCommentsByUid(uid, options = {}) { + const asesorias = await getAsesoriasByUid(uid, options); + return (asesorias ?? []).filter((asesoria) => asesoria.comment?.trim()); +} + +export async function getAsesoriasByUidAndRating(uidUser, uidPeer = null, options = {}) { + return await withCache( + cacheKeys.asesoriasPendingRating(uidUser, uidPeer ?? 'all'), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await asesoriaDb.getAsesoriasByUidAndRating(uidUser, uidPeer) + ); +} + +export async function updateAsesoria(id, data) { + const result = await asesoriaDb.updateAsesoria(id, data); + await invalidateAsesoriaCaches(); + return result; +} + +export async function getTotalAsesorias(startDate = null, endDate = null, options = {}) { + const asesorias = await getAsesorias(startDate, endDate, options); + return (asesorias ?? []).length; +} + +export async function getAsesoriasCountByUser(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const userAsesoriasSet = new Set((asesorias ?? []).map(doc => doc.userInfo?.uid).filter(Boolean)); + return userAsesoriasSet.size; +} + +export async function getAsesoriasCountByArea(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const areasCount = {}; + + (asesorias ?? []).forEach((asesoria) => { + const subjectArea = asesoria?.subject?.area; + const userUid = asesoria?.userInfo?.uid; + + if (!subjectArea || !userUid) { + return; + } + + if (!areasCount[subjectArea]) { + areasCount[subjectArea] = { + totalAsesorias: 0, + userUids: new Set() + }; + } + + areasCount[subjectArea].totalAsesorias++; + areasCount[subjectArea].userUids.add(userUid); + }); + + return Object.keys(areasCount).map(area => ({ + area, + totalAsesorias: areasCount[area].totalAsesorias, + totalUniqueUsers: areasCount[area].userUids.size + })); +} + +export async function getAsesoriasCountByCampus(options = {}) { + const asesorias = await getAsesorias(null, null, options); + const campusCount = {}; + + (asesorias ?? []).forEach((asesoria) => { + const campus = asesoria?.userInfo?.campus; + if (campus) { + campusCount[campus] = (campusCount[campus] || 0) + 1; + } + }); + + return Object.keys(campusCount).map(campus => ({ + campus, + totalAsesorias: campusCount[campus] + })); +} + +export async function deleteOldAsesorias() { + const result = await asesoriaDb.deleteOldAsesorias(); + await invalidateAsesoriaCaches(); + return result; +} diff --git a/src/firebase/db/attendance.cached.js b/src/firebase/db/attendance.cached.js deleted file mode 100644 index 7dc11e5b6..000000000 --- a/src/firebase/db/attendance.cached.js +++ /dev/null @@ -1,99 +0,0 @@ -import * as attendanceDb from './attendance.legacy'; -import { attendanceDateTag, CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; -import { invalidateCacheTags, withCache } from '../cache/cache'; - -function getCurrentDateFormatted() { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -function resolveAttendanceTtl(dateString) { - return dateString === getCurrentDateFormatted() ? CACHE_TTL_MS.ATTENDANCE_TODAY : CACHE_TTL_MS.ATTENDANCE_DAY; -} - -async function invalidateAttendanceForDate(dateString) { - await invalidateCacheTags([CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)]); -} - -export async function getTodaysReport(options = {}) { - const today = getCurrentDateFormatted(); - - return await withCache( - cacheKeys.attendanceToday(today), - { - ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, - persist: false, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] - }, - async () => await attendanceDb.getTodaysReport() - ); -} - -export async function updateReport(userInfo, report) { - const result = await attendanceDb.updateReport(userInfo, report); - await invalidateAttendanceForDate(getCurrentDateFormatted()); - return result; -} - -export async function updateReportByDate(userInfo, date, report) { - const result = await attendanceDb.updateReportByDate(userInfo, date, report); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - await invalidateAttendanceForDate(`${year}-${month}-${day}`); - return result; -} - -export async function addRegister(userInfo, date) { - const result = await attendanceDb.addRegister(userInfo, date); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - await invalidateAttendanceForDate(`${year}-${month}-${day}`); - return result; -} - -export async function getStudentReport(uid, options = {}) { - const today = getCurrentDateFormatted(); - - return await withCache( - cacheKeys.attendanceStudent(uid, today), - { - ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, - persist: false, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] - }, - async () => await attendanceDb.getStudentReport(uid) - ); -} - -export async function getReportByDate(dateString, options = {}) { - return await withCache( - cacheKeys.attendanceByDate(dateString), - { - ttlMs: resolveAttendanceTtl(dateString), - persist: dateString !== getCurrentDateFormatted(), - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)] - }, - async () => await attendanceDb.getReportByDate(dateString) - ); -} - -export async function getReportByDateRange(startDate, endDate, options = {}) { - return await withCache( - cacheKeys.attendanceRange(startDate, endDate), - { - ttlMs: CACHE_TTL_MS.ATTENDANCE_RANGE, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ATTENDANCE] - }, - async () => await attendanceDb.getReportByDateRange(startDate, endDate) - ); -} diff --git a/src/firebase/db/attendance.js b/src/firebase/db/attendance.js index 4505784b8..7dc11e5b6 100644 --- a/src/firebase/db/attendance.js +++ b/src/firebase/db/attendance.js @@ -1 +1,99 @@ -export * from './attendance.cached'; +import * as attendanceDb from './attendance.legacy'; +import { attendanceDateTag, CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; +import { invalidateCacheTags, withCache } from '../cache/cache'; + +function getCurrentDateFormatted() { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function resolveAttendanceTtl(dateString) { + return dateString === getCurrentDateFormatted() ? CACHE_TTL_MS.ATTENDANCE_TODAY : CACHE_TTL_MS.ATTENDANCE_DAY; +} + +async function invalidateAttendanceForDate(dateString) { + await invalidateCacheTags([CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)]); +} + +export async function getTodaysReport(options = {}) { + const today = getCurrentDateFormatted(); + + return await withCache( + cacheKeys.attendanceToday(today), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] + }, + async () => await attendanceDb.getTodaysReport() + ); +} + +export async function updateReport(userInfo, report) { + const result = await attendanceDb.updateReport(userInfo, report); + await invalidateAttendanceForDate(getCurrentDateFormatted()); + return result; +} + +export async function updateReportByDate(userInfo, date, report) { + const result = await attendanceDb.updateReportByDate(userInfo, date, report); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + await invalidateAttendanceForDate(`${year}-${month}-${day}`); + return result; +} + +export async function addRegister(userInfo, date) { + const result = await attendanceDb.addRegister(userInfo, date); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + await invalidateAttendanceForDate(`${year}-${month}-${day}`); + return result; +} + +export async function getStudentReport(uid, options = {}) { + const today = getCurrentDateFormatted(); + + return await withCache( + cacheKeys.attendanceStudent(uid, today), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_TODAY, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] + }, + async () => await attendanceDb.getStudentReport(uid) + ); +} + +export async function getReportByDate(dateString, options = {}) { + return await withCache( + cacheKeys.attendanceByDate(dateString), + { + ttlMs: resolveAttendanceTtl(dateString), + persist: dateString !== getCurrentDateFormatted(), + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)] + }, + async () => await attendanceDb.getReportByDate(dateString) + ); +} + +export async function getReportByDateRange(startDate, endDate, options = {}) { + return await withCache( + cacheKeys.attendanceRange(startDate, endDate), + { + ttlMs: CACHE_TTL_MS.ATTENDANCE_RANGE, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ATTENDANCE] + }, + async () => await attendanceDb.getReportByDateRange(startDate, endDate) + ); +} diff --git a/src/firebase/db/campuses.cached.js b/src/firebase/db/campuses.cached.js deleted file mode 100644 index df9a2b948..000000000 --- a/src/firebase/db/campuses.cached.js +++ /dev/null @@ -1,29 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, -} from 'firebase/firestore'; -import { withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -export async function getCampuses(options = {}) { - return await withCache( - cacheKeys.campuses(), - { - ttlMs: CACHE_TTL_MS.CAMPUSES, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.CAMPUSES] - }, - async () => { - const campusesRef = collection(firestoreDB, "schools/tec.mx/campus"); - const docsSnap = await getDocs(campusesRef); - - if (docsSnap) { - return docsSnap.docs.map(doc => doc.data()); - } - - return null; - } - ); -} diff --git a/src/firebase/db/campuses.js b/src/firebase/db/campuses.js index ae8dc3736..df9a2b948 100644 --- a/src/firebase/db/campuses.js +++ b/src/firebase/db/campuses.js @@ -1 +1,29 @@ -export * from './campuses.cached'; +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, +} from 'firebase/firestore'; +import { withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getCampuses(options = {}) { + return await withCache( + cacheKeys.campuses(), + { + ttlMs: CACHE_TTL_MS.CAMPUSES, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.CAMPUSES] + }, + async () => { + const campusesRef = collection(firestoreDB, "schools/tec.mx/campus"); + const docsSnap = await getDocs(campusesRef); + + if (docsSnap) { + return docsSnap.docs.map(doc => doc.data()); + } + + return null; + } + ); +} diff --git a/src/firebase/db/maeteca.cached.js b/src/firebase/db/maeteca.cached.js deleted file mode 100644 index 709b38b21..000000000 --- a/src/firebase/db/maeteca.cached.js +++ /dev/null @@ -1,419 +0,0 @@ -// Filtra videos por texto en título o descripción, normalizado (MaesActivos style) -import { normalize } from '@/utils/HorarioUtils'; -export function filterVideosByText(videos, text) { - if (!Array.isArray(videos)) return []; - const normalizeText = typeof text === 'string' ? text : ''; - const query = normalize(normalizeText || ''); - if (!query) return videos; - return videos.filter(video => { - const title = normalize(video.Titulo || ''); - const info = normalize(video.Informacion || ''); - return title.includes(query) || info.includes(query); - }); -} -import { firestoreDB } from "../../main"; -import { - getDocs, - addDoc, - collection, - serverTimestamp -} from 'firebase/firestore'; -import { getCurrentUser } from './users'; -import { invalidateCacheTags, withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -export const VIDEO_MANAGER_ROLES = ['admin', 'tec', 'coordi']; - -function assertVideoPermissions(user) { - if (!VIDEO_MANAGER_ROLES.includes(user?.role)) { - const role = user?.role ?? 'unknown'; - throw new Error(`Insufficient permissions for role '${role}' to manage Maeteca videos`); - } -} - -async function invalidateVideoCaches() { - await invalidateCacheTags([CACHE_TAGS.VIDEOS]); -} - -async function fetchAllVideosFresh() { - const videosRef = collection(firestoreDB, "videos"); - const snapshot = await getDocs(videosRef); - - if (snapshot.empty) { - console.log("No se encontraron videos "); - return []; - } - - return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })); -} - -// CREAR documentos -export async function addVideoToMaeteca(videoData, { invalidate = true } = {}) { - try { - const user = await getCurrentUser(); - if (!user) throw new Error('No authenticated user for write'); - - assertVideoPermissions(user); - - const payload = { - ...videoData, - createdBy: { uid: user.uid, role: user.role }, - createdAt: serverTimestamp() - }; - - const docRef = await addDoc(collection(firestoreDB, "videos"), payload); - if (invalidate) { - await invalidateVideoCaches(); - } - console.log("Documento agregado con ID:", docRef.id); - return docRef; - } catch (error) { - console.error("Error agregando documento:", error); - throw error; - } -} - - -// LEER todos los videos -export async function getAllVideos(options = {}) { - try { - return await withCache( - cacheKeys.videosAll(), - { - ttlMs: CACHE_TTL_MS.VIDEOS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.VIDEOS] - }, - fetchAllVideosFresh - ); - } catch (error) { - console.error("Error obteniendo videos:", error); - throw error; - } -} - -export function canUserManageVideos(role) { - return VIDEO_MANAGER_ROLES.includes(role ?? ''); -} - -// Constantes reutilizables por la UI -export const AVAILABLE_TAG_OPTIONS = [ - { label: '#maeteca', value: 'maeteca' }, - { label: '#general', value: 'general' }, - { label: '#tutorial', value: 'tutorial' }, - { label: '#matematicas', value: 'matematicas' }, - { label: '#Quimica', value: 'Quimica' }, - { label: '#Ciencias sociales', value: 'Ciencias sociales' }, - { label: '#Creatividad', value: 'Creatividad' }, - { label: '#Fisica', value: 'Fisica' } -]; - -export const TAGS = [ - { name: 'Programación', code: 'PROG' }, - { name: 'Matemáticas', code: 'MATH' }, - { name: 'Física', code: 'PHY' } -]; - -export const VIDEO_SUBJECTS = [ - { name: 'General', code: 'general' }, - { name: 'Matemáticas', code: 'math' }, - { name: 'Programación', code: 'prog' } -]; - -export const VIDEO_CAREERS = [ - { name: 'Todas', code: 'all' }, - { name: 'ITC', code: 'itc' }, - { name: 'IMT', code: 'imt' }, - { name: 'IDS', code: 'ids' } -]; - -export const SEMESTERS = [ - { name: 'Primer Semestre', code: '1' }, - { name: 'Segundo Semestre', code: '2' }, - { name: 'Tercer Semestre', code: '3' } -]; - -export const TYPES = [ - { name: 'Video', code: 'VID' }, - { name: 'Artículo', code: 'ART' }, - { name: 'Libro', code: 'BOOK' } -]; - -// Extrae el id de YouTube desde varias formas de URL -export function extractYoutubeId(url) { - if (!url || typeof url !== 'string') return null; - const patterns = [ - /youtube\.com\/watch\?v=([^&]+)/, - /youtube\.com\/embed\/([^?]+)/, - /youtu\.be\/([^?]+)/ - ]; - for (const p of patterns) { - const m = url.match(p); - if (m?.[1]) return m[1]; - } - return null; -} - -export async function loadMaetecaVideos(options = {}) { - const data = await getAllVideos(options); - return Array.isArray(data) ? data : []; -} - -// Obtener un video por su id de documento -export async function getVideoById(id, options = {}) { - if (!id) return null; - try { - return await withCache( - cacheKeys.videoById(id), - { - ttlMs: CACHE_TTL_MS.VIDEOS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.VIDEOS] - }, - async () => { - const videos = await getAllVideos(options); - return (videos ?? []).find((video) => video.id === id) ?? null; - } - ); - } catch (error) { - console.error(`Error obteniendo video ${id}:`, error); - throw error; - } -} - -// Genera la URL de embed (iframe) para un video de YouTube -export function getVideoEmbedUrl(video) { - if (!video) return null; - const url = typeof video === 'string' ? video : video.Video; - if (!url) return null; - const id = extractYoutubeId(url); - if (id) return `https://www.youtube.com/embed/${id}`; - return url; -} - -export function getVideoThumbnail(video) { - if (!video) return null; - if (video.Thumbnail) return video.Thumbnail; - const url = video.Video; - if (!url) return null; - const id = extractYoutubeId(url); - return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null; -} - -export function openVideo(url) { - if (!url || typeof window === 'undefined') return; - window.open(url, '_blank', 'noopener'); -} - -export function handleThumbnailKey(event, url) { - if (!url) return; - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openVideo(url); - } -} - -// BUSCAR por array "Relacionado" -export async function getVideosByRelated(relacionadoItem, options = {}) { - try { - return await withCache( - cacheKeys.videosByRelated(relacionadoItem), - { - ttlMs: CACHE_TTL_MS.VIDEOS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.VIDEOS] - }, - async () => { - const videos = await getAllVideos(options); - return (videos ?? []).filter((video) => Array.isArray(video.Relacionado) && video.Relacionado.includes(relacionadoItem)); - } - ); - } catch (error) { - console.error("Error buscando por relacionado:", error); - throw error; - } -} - -// Actualiza un documento de video -// (removed optional admin/update/delete/filter/subscribe helpers per request) - - -export async function createSampleVideos() { - const user = await getCurrentUser(); - if (!user) throw new Error('No authenticated user for write'); - assertVideoPermissions(user); - console.log("Usuario autenticado:", user); // Verifica la información del usuario - - const samples = [ - { - "Informacion": "Video explicativo sobre el Método de Euler en modelación matemática.", - "Relacionado": ["método de euler", "modelación matemática", "aproximación numérica"], - "Titulo": "Método de Euler", - "Video": "https://youtu.be/B9YR-GXGncw" - }, - { - "Informacion": "Video sobre funciones polinomiales y cómo calcular su derivada.", - "Relacionado": ["funciones polinomiales", "derivadas", "modelación matemática"], - "Titulo": "Funciones polinomiales y su derivada", - "Video": "https://youtu.be/2ntPaw4vkc8" - }, - { - "Informacion": "Explicación de la función exponencial y el cálculo de su derivada.", - "Relacionado": ["función exponencial", "derivadas", "modelación matemática"], - "Titulo": "Función exponencial y su derivada", - "Video": "https://youtu.be/6V_LmXGCbSg" - }, - { - "Informacion": "Derivación de constantes y suma de funciones en modelación matemática.", - "Relacionado": ["derivadas", "constantes", "suma de funciones"], - "Titulo": "Derivada de constante y suma de funciones", - "Video": "https://youtu.be/9ghBNnZ6t7g" - }, - { - "Informacion": "Cómo derivar el producto de dos funciones.", - "Relacionado": ["derivadas", "producto de funciones", "regla del producto"], - "Titulo": "Derivada de producto de funciones", - "Video": "https://youtu.be/8XS55_kOlmk" - }, - { - "Informacion": "Derivación de cociente de funciones con ejemplos paso a paso.", - "Relacionado": ["derivadas", "cociente de funciones", "regla del cociente"], - "Titulo": "Derivada de cociente de funciones", - "Video": "https://youtu.be/cw9zLw6k3dA" - }, - { - "Informacion": "Uso de la regla de la cadena y derivación implícita.", - "Relacionado": ["regla de la cadena", "derivación implícita", "derivadas"], - "Titulo": "Regla de la cadena y derivación implícita", - "Video": "https://youtu.be/aGRJEaYh9Ws" - }, - { - "Informacion": "Cambio de variable en integración y derivación.", - "Relacionado": ["cambio de variable", "integrales", "derivadas"], - "Titulo": "Cambio de variable", - "Video": "https://youtu.be/pMLrvpBF_4o" - }, - { - "Informacion": "Integración por partes aplicada a problemas de ingeniería.", - "Relacionado": ["integrales", "integración por partes", "modelación matemática"], - "Titulo": "Integración por partes", - "Video": "https://youtu.be/VzxmmKKY3GM" - }, - { - "Informacion": "Concepto y cálculo del plano tangente en superficies.", - "Relacionado": ["plano tangente", "derivadas parciales", "superficies"], - "Titulo": "Plano tangente", - "Video": "https://youtu.be/ZzwEwfFTP7Q" - }, - { - "Informacion": "Cómo calcular derivadas direccionales y su interpretación geométrica.", - "Relacionado": ["derivada direccional", "gradiente", "superficies"], - "Titulo": "Derivada direccional", - "Video": "https://youtu.be/RbySC1xgM9o" - }, - { - "Informacion": "Uso de la transformada de Laplace en ecuaciones diferenciales.", - "Relacionado": ["transformada de laplace", "ecuaciones diferenciales", "modelación dinámica"], - "Titulo": "Transformada de Laplace", - "Video": "https://youtu.be/-7vsj9f24-c" - }, - { - "Informacion": "Conceptos básicos y operaciones con matrices.", - "Relacionado": ["matrices", "álgebra lineal", "operaciones matriciales"], - "Titulo": "Matrices: conceptos básicos y operaciones", - "Video": "https://youtu.be/krpLf9XP4vs" - }, - { - "Informacion": "Cómo calcular la matriz de cofactores.", - "Relacionado": ["matrices", "cofactores", "determinantes"], - "Titulo": "Matriz de cofactores", - "Video": "https://youtu.be/9uZ96OEcTuc" - }, - { - "Informacion": "Definición y obtención de la matriz adjunta.", - "Relacionado": ["matrices", "matriz adjunta", "álgebra lineal"], - "Titulo": "Matriz adjunta", - "Video": "https://youtu.be/PhJwWFWQQiY" - }, - { - "Informacion": "Cálculo de la matriz inversa paso a paso.", - "Relacionado": ["matrices", "inversa de matriz", "determinantes"], - "Titulo": "Matriz inversa", - "Video": "https://youtu.be/nyFLyIeeHmA" - }, - { - "Informacion": "Cálculo del determinante en sistemas de ecuaciones nxn.", - "Relacionado": ["determinantes", "sistemas lineales", "álgebra lineal"], - "Titulo": "Cálculo del determinante en sistemas nxn", - "Video": "https://youtu.be/XgWuTkx0CjA" - }, - { - "Informacion": "Resolución de sistemas lineales por el método de Gauss-Jordan.", - "Relacionado": ["gauss-jordan", "sistemas lineales", "álgebra lineal"], - "Titulo": "Método de Gauss-Jordan", - "Video": "https://youtu.be/MslG1TrSQO4" - }, - { - "Informacion": "Cálculo con operadores aplicado a ingeniería.", - "Relacionado": ["operadores", "pensamiento computacional", "matemáticas aplicadas"], - "Titulo": "Cálculo con operadores", - "Video": "https://youtu.be/AiJIcK3yIZw" - }, - { - "Informacion": "Uso del método Solver para resolver problemas de programación lineal.", - "Relacionado": ["programación lineal", "solver", "análisis de decisiones"], - "Titulo": "Método Solver en programación lineal", - "Video": "https://youtu.be/c-DPPmNef0Y" - }, - { - "Informacion": "Representación gráfica de modelos de programación lineal.", - "Relacionado": ["programación lineal", "gráficas", "análisis de decisiones"], - "Titulo": "Gráficas en programación lineal", - "Video": "https://youtu.be/RQ2pSyjH-64" - }, - { - "Informacion": "Uso del comando Array en AutoCAD.", - "Relacionado": ["autocad", "comando array", "dibujo asistido"], - "Titulo": "Comando Array", - "Video": "https://youtu.be/t3W_DDSnTDU" - }, - { - "Informacion": "Cómo usar el comando Offset para crear copias paralelas de objetos.", - "Relacionado": ["autocad", "offset", "diseño técnico"], - "Titulo": "Comando Offset", - "Video": "https://youtu.be/FXU5ZzXTwRQ" - }, - { - "Informacion": "Cálculo del área de una figura en AutoCAD.", - "Relacionado": ["autocad", "área", "medición"], - "Titulo": "Cálculo del área de una figura", - "Video": "https://youtu.be/IpBFI7Zhymg" - }, - { - "Informacion": "Uso de los comandos Trim y Fillet para edición de figuras.", - "Relacionado": ["autocad", "trim", "fillet", "dibujo 2D"], - "Titulo": "Comandos Trim y Fillet", - "Video": "https://youtu.be/fA-z6OjDvMQ" - } -]; - - const insertedIds = []; - try { - for (const item of samples) { - // addVideoToMaeteca will attach createdBy and createdAt - const ref = await addVideoToMaeteca(item, { invalidate: false }); - if (ref && ref.id) insertedIds.push(ref.id); - console.log(`Agregado: ${item.Titulo} -> ${ref?.id}`); - } - await invalidateVideoCaches(); - console.log(`Videos de ejemplo creados Total: ${insertedIds.length}`); - return insertedIds; - } catch (error) { - console.error("Error creando videos de ejemplo:", error); - throw error; - } -} - diff --git a/src/firebase/db/maeteca.js b/src/firebase/db/maeteca.js index c9a8da327..709b38b21 100644 --- a/src/firebase/db/maeteca.js +++ b/src/firebase/db/maeteca.js @@ -1 +1,419 @@ -export * from './maeteca.cached'; +// Filtra videos por texto en título o descripción, normalizado (MaesActivos style) +import { normalize } from '@/utils/HorarioUtils'; +export function filterVideosByText(videos, text) { + if (!Array.isArray(videos)) return []; + const normalizeText = typeof text === 'string' ? text : ''; + const query = normalize(normalizeText || ''); + if (!query) return videos; + return videos.filter(video => { + const title = normalize(video.Titulo || ''); + const info = normalize(video.Informacion || ''); + return title.includes(query) || info.includes(query); + }); +} +import { firestoreDB } from "../../main"; +import { + getDocs, + addDoc, + collection, + serverTimestamp +} from 'firebase/firestore'; +import { getCurrentUser } from './users'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export const VIDEO_MANAGER_ROLES = ['admin', 'tec', 'coordi']; + +function assertVideoPermissions(user) { + if (!VIDEO_MANAGER_ROLES.includes(user?.role)) { + const role = user?.role ?? 'unknown'; + throw new Error(`Insufficient permissions for role '${role}' to manage Maeteca videos`); + } +} + +async function invalidateVideoCaches() { + await invalidateCacheTags([CACHE_TAGS.VIDEOS]); +} + +async function fetchAllVideosFresh() { + const videosRef = collection(firestoreDB, "videos"); + const snapshot = await getDocs(videosRef); + + if (snapshot.empty) { + console.log("No se encontraron videos "); + return []; + } + + return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })); +} + +// CREAR documentos +export async function addVideoToMaeteca(videoData, { invalidate = true } = {}) { + try { + const user = await getCurrentUser(); + if (!user) throw new Error('No authenticated user for write'); + + assertVideoPermissions(user); + + const payload = { + ...videoData, + createdBy: { uid: user.uid, role: user.role }, + createdAt: serverTimestamp() + }; + + const docRef = await addDoc(collection(firestoreDB, "videos"), payload); + if (invalidate) { + await invalidateVideoCaches(); + } + console.log("Documento agregado con ID:", docRef.id); + return docRef; + } catch (error) { + console.error("Error agregando documento:", error); + throw error; + } +} + + +// LEER todos los videos +export async function getAllVideos(options = {}) { + try { + return await withCache( + cacheKeys.videosAll(), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + fetchAllVideosFresh + ); + } catch (error) { + console.error("Error obteniendo videos:", error); + throw error; + } +} + +export function canUserManageVideos(role) { + return VIDEO_MANAGER_ROLES.includes(role ?? ''); +} + +// Constantes reutilizables por la UI +export const AVAILABLE_TAG_OPTIONS = [ + { label: '#maeteca', value: 'maeteca' }, + { label: '#general', value: 'general' }, + { label: '#tutorial', value: 'tutorial' }, + { label: '#matematicas', value: 'matematicas' }, + { label: '#Quimica', value: 'Quimica' }, + { label: '#Ciencias sociales', value: 'Ciencias sociales' }, + { label: '#Creatividad', value: 'Creatividad' }, + { label: '#Fisica', value: 'Fisica' } +]; + +export const TAGS = [ + { name: 'Programación', code: 'PROG' }, + { name: 'Matemáticas', code: 'MATH' }, + { name: 'Física', code: 'PHY' } +]; + +export const VIDEO_SUBJECTS = [ + { name: 'General', code: 'general' }, + { name: 'Matemáticas', code: 'math' }, + { name: 'Programación', code: 'prog' } +]; + +export const VIDEO_CAREERS = [ + { name: 'Todas', code: 'all' }, + { name: 'ITC', code: 'itc' }, + { name: 'IMT', code: 'imt' }, + { name: 'IDS', code: 'ids' } +]; + +export const SEMESTERS = [ + { name: 'Primer Semestre', code: '1' }, + { name: 'Segundo Semestre', code: '2' }, + { name: 'Tercer Semestre', code: '3' } +]; + +export const TYPES = [ + { name: 'Video', code: 'VID' }, + { name: 'Artículo', code: 'ART' }, + { name: 'Libro', code: 'BOOK' } +]; + +// Extrae el id de YouTube desde varias formas de URL +export function extractYoutubeId(url) { + if (!url || typeof url !== 'string') return null; + const patterns = [ + /youtube\.com\/watch\?v=([^&]+)/, + /youtube\.com\/embed\/([^?]+)/, + /youtu\.be\/([^?]+)/ + ]; + for (const p of patterns) { + const m = url.match(p); + if (m?.[1]) return m[1]; + } + return null; +} + +export async function loadMaetecaVideos(options = {}) { + const data = await getAllVideos(options); + return Array.isArray(data) ? data : []; +} + +// Obtener un video por su id de documento +export async function getVideoById(id, options = {}) { + if (!id) return null; + try { + return await withCache( + cacheKeys.videoById(id), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + async () => { + const videos = await getAllVideos(options); + return (videos ?? []).find((video) => video.id === id) ?? null; + } + ); + } catch (error) { + console.error(`Error obteniendo video ${id}:`, error); + throw error; + } +} + +// Genera la URL de embed (iframe) para un video de YouTube +export function getVideoEmbedUrl(video) { + if (!video) return null; + const url = typeof video === 'string' ? video : video.Video; + if (!url) return null; + const id = extractYoutubeId(url); + if (id) return `https://www.youtube.com/embed/${id}`; + return url; +} + +export function getVideoThumbnail(video) { + if (!video) return null; + if (video.Thumbnail) return video.Thumbnail; + const url = video.Video; + if (!url) return null; + const id = extractYoutubeId(url); + return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null; +} + +export function openVideo(url) { + if (!url || typeof window === 'undefined') return; + window.open(url, '_blank', 'noopener'); +} + +export function handleThumbnailKey(event, url) { + if (!url) return; + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + openVideo(url); + } +} + +// BUSCAR por array "Relacionado" +export async function getVideosByRelated(relacionadoItem, options = {}) { + try { + return await withCache( + cacheKeys.videosByRelated(relacionadoItem), + { + ttlMs: CACHE_TTL_MS.VIDEOS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.VIDEOS] + }, + async () => { + const videos = await getAllVideos(options); + return (videos ?? []).filter((video) => Array.isArray(video.Relacionado) && video.Relacionado.includes(relacionadoItem)); + } + ); + } catch (error) { + console.error("Error buscando por relacionado:", error); + throw error; + } +} + +// Actualiza un documento de video +// (removed optional admin/update/delete/filter/subscribe helpers per request) + + +export async function createSampleVideos() { + const user = await getCurrentUser(); + if (!user) throw new Error('No authenticated user for write'); + assertVideoPermissions(user); + console.log("Usuario autenticado:", user); // Verifica la información del usuario + + const samples = [ + { + "Informacion": "Video explicativo sobre el Método de Euler en modelación matemática.", + "Relacionado": ["método de euler", "modelación matemática", "aproximación numérica"], + "Titulo": "Método de Euler", + "Video": "https://youtu.be/B9YR-GXGncw" + }, + { + "Informacion": "Video sobre funciones polinomiales y cómo calcular su derivada.", + "Relacionado": ["funciones polinomiales", "derivadas", "modelación matemática"], + "Titulo": "Funciones polinomiales y su derivada", + "Video": "https://youtu.be/2ntPaw4vkc8" + }, + { + "Informacion": "Explicación de la función exponencial y el cálculo de su derivada.", + "Relacionado": ["función exponencial", "derivadas", "modelación matemática"], + "Titulo": "Función exponencial y su derivada", + "Video": "https://youtu.be/6V_LmXGCbSg" + }, + { + "Informacion": "Derivación de constantes y suma de funciones en modelación matemática.", + "Relacionado": ["derivadas", "constantes", "suma de funciones"], + "Titulo": "Derivada de constante y suma de funciones", + "Video": "https://youtu.be/9ghBNnZ6t7g" + }, + { + "Informacion": "Cómo derivar el producto de dos funciones.", + "Relacionado": ["derivadas", "producto de funciones", "regla del producto"], + "Titulo": "Derivada de producto de funciones", + "Video": "https://youtu.be/8XS55_kOlmk" + }, + { + "Informacion": "Derivación de cociente de funciones con ejemplos paso a paso.", + "Relacionado": ["derivadas", "cociente de funciones", "regla del cociente"], + "Titulo": "Derivada de cociente de funciones", + "Video": "https://youtu.be/cw9zLw6k3dA" + }, + { + "Informacion": "Uso de la regla de la cadena y derivación implícita.", + "Relacionado": ["regla de la cadena", "derivación implícita", "derivadas"], + "Titulo": "Regla de la cadena y derivación implícita", + "Video": "https://youtu.be/aGRJEaYh9Ws" + }, + { + "Informacion": "Cambio de variable en integración y derivación.", + "Relacionado": ["cambio de variable", "integrales", "derivadas"], + "Titulo": "Cambio de variable", + "Video": "https://youtu.be/pMLrvpBF_4o" + }, + { + "Informacion": "Integración por partes aplicada a problemas de ingeniería.", + "Relacionado": ["integrales", "integración por partes", "modelación matemática"], + "Titulo": "Integración por partes", + "Video": "https://youtu.be/VzxmmKKY3GM" + }, + { + "Informacion": "Concepto y cálculo del plano tangente en superficies.", + "Relacionado": ["plano tangente", "derivadas parciales", "superficies"], + "Titulo": "Plano tangente", + "Video": "https://youtu.be/ZzwEwfFTP7Q" + }, + { + "Informacion": "Cómo calcular derivadas direccionales y su interpretación geométrica.", + "Relacionado": ["derivada direccional", "gradiente", "superficies"], + "Titulo": "Derivada direccional", + "Video": "https://youtu.be/RbySC1xgM9o" + }, + { + "Informacion": "Uso de la transformada de Laplace en ecuaciones diferenciales.", + "Relacionado": ["transformada de laplace", "ecuaciones diferenciales", "modelación dinámica"], + "Titulo": "Transformada de Laplace", + "Video": "https://youtu.be/-7vsj9f24-c" + }, + { + "Informacion": "Conceptos básicos y operaciones con matrices.", + "Relacionado": ["matrices", "álgebra lineal", "operaciones matriciales"], + "Titulo": "Matrices: conceptos básicos y operaciones", + "Video": "https://youtu.be/krpLf9XP4vs" + }, + { + "Informacion": "Cómo calcular la matriz de cofactores.", + "Relacionado": ["matrices", "cofactores", "determinantes"], + "Titulo": "Matriz de cofactores", + "Video": "https://youtu.be/9uZ96OEcTuc" + }, + { + "Informacion": "Definición y obtención de la matriz adjunta.", + "Relacionado": ["matrices", "matriz adjunta", "álgebra lineal"], + "Titulo": "Matriz adjunta", + "Video": "https://youtu.be/PhJwWFWQQiY" + }, + { + "Informacion": "Cálculo de la matriz inversa paso a paso.", + "Relacionado": ["matrices", "inversa de matriz", "determinantes"], + "Titulo": "Matriz inversa", + "Video": "https://youtu.be/nyFLyIeeHmA" + }, + { + "Informacion": "Cálculo del determinante en sistemas de ecuaciones nxn.", + "Relacionado": ["determinantes", "sistemas lineales", "álgebra lineal"], + "Titulo": "Cálculo del determinante en sistemas nxn", + "Video": "https://youtu.be/XgWuTkx0CjA" + }, + { + "Informacion": "Resolución de sistemas lineales por el método de Gauss-Jordan.", + "Relacionado": ["gauss-jordan", "sistemas lineales", "álgebra lineal"], + "Titulo": "Método de Gauss-Jordan", + "Video": "https://youtu.be/MslG1TrSQO4" + }, + { + "Informacion": "Cálculo con operadores aplicado a ingeniería.", + "Relacionado": ["operadores", "pensamiento computacional", "matemáticas aplicadas"], + "Titulo": "Cálculo con operadores", + "Video": "https://youtu.be/AiJIcK3yIZw" + }, + { + "Informacion": "Uso del método Solver para resolver problemas de programación lineal.", + "Relacionado": ["programación lineal", "solver", "análisis de decisiones"], + "Titulo": "Método Solver en programación lineal", + "Video": "https://youtu.be/c-DPPmNef0Y" + }, + { + "Informacion": "Representación gráfica de modelos de programación lineal.", + "Relacionado": ["programación lineal", "gráficas", "análisis de decisiones"], + "Titulo": "Gráficas en programación lineal", + "Video": "https://youtu.be/RQ2pSyjH-64" + }, + { + "Informacion": "Uso del comando Array en AutoCAD.", + "Relacionado": ["autocad", "comando array", "dibujo asistido"], + "Titulo": "Comando Array", + "Video": "https://youtu.be/t3W_DDSnTDU" + }, + { + "Informacion": "Cómo usar el comando Offset para crear copias paralelas de objetos.", + "Relacionado": ["autocad", "offset", "diseño técnico"], + "Titulo": "Comando Offset", + "Video": "https://youtu.be/FXU5ZzXTwRQ" + }, + { + "Informacion": "Cálculo del área de una figura en AutoCAD.", + "Relacionado": ["autocad", "área", "medición"], + "Titulo": "Cálculo del área de una figura", + "Video": "https://youtu.be/IpBFI7Zhymg" + }, + { + "Informacion": "Uso de los comandos Trim y Fillet para edición de figuras.", + "Relacionado": ["autocad", "trim", "fillet", "dibujo 2D"], + "Titulo": "Comandos Trim y Fillet", + "Video": "https://youtu.be/fA-z6OjDvMQ" + } +]; + + const insertedIds = []; + try { + for (const item of samples) { + // addVideoToMaeteca will attach createdBy and createdAt + const ref = await addVideoToMaeteca(item, { invalidate: false }); + if (ref && ref.id) insertedIds.push(ref.id); + console.log(`Agregado: ${item.Titulo} -> ${ref?.id}`); + } + await invalidateVideoCaches(); + console.log(`Videos de ejemplo creados Total: ${insertedIds.length}`); + return insertedIds; + } catch (error) { + console.error("Error creando videos de ejemplo:", error); + throw error; + } +} + diff --git a/src/firebase/db/majors.cached.js b/src/firebase/db/majors.cached.js deleted file mode 100644 index 9978e16db..000000000 --- a/src/firebase/db/majors.cached.js +++ /dev/null @@ -1,27 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, -} from 'firebase/firestore'; -import { withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -export async function getMajors(options = {}) { - return await withCache( - cacheKeys.majors(), - { - ttlMs: CACHE_TTL_MS.MAJORS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.MAJORS] - }, - async () => { - const majorsRef = collection(firestoreDB, "schools/tec.mx/majors"); - const docsSnap = await getDocs(majorsRef); - if (docsSnap) { - return docsSnap.docs.map(doc => doc.data()); - } - return null; - } - ); -} diff --git a/src/firebase/db/majors.js b/src/firebase/db/majors.js index 59fdff405..9978e16db 100644 --- a/src/firebase/db/majors.js +++ b/src/firebase/db/majors.js @@ -1 +1,27 @@ -export * from './majors.cached'; +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, +} from 'firebase/firestore'; +import { withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getMajors(options = {}) { + return await withCache( + cacheKeys.majors(), + { + ttlMs: CACHE_TTL_MS.MAJORS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.MAJORS] + }, + async () => { + const majorsRef = collection(firestoreDB, "schools/tec.mx/majors"); + const docsSnap = await getDocs(majorsRef); + if (docsSnap) { + return docsSnap.docs.map(doc => doc.data()); + } + return null; + } + ); +} diff --git a/src/firebase/db/subjects.cached.js b/src/firebase/db/subjects.cached.js deleted file mode 100644 index 55b1e5101..000000000 --- a/src/firebase/db/subjects.cached.js +++ /dev/null @@ -1,45 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - collection, - getDocs, - query, - orderBy, -} from 'firebase/firestore'; -import { doc, setDoc, deleteDoc } from 'firebase/firestore'; -import { invalidateCacheTags, withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -export async function getSubjects(options = {}) { - return await withCache( - cacheKeys.subjects(), - { - ttlMs: CACHE_TTL_MS.SUBJECTS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.SUBJECTS] - }, - async () => { - const subjectsRef = collection(firestoreDB, "schools/tec.mx/subjects"); - const q = query(subjectsRef, orderBy("name")); - const docsSnap = await getDocs(q); - - if (!docsSnap.empty) { - return docsSnap.docs.map(doc => doc.data()); - } - - return null; - } - ); -} - -export async function addSubject(subject) { - const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subject.id}`); - await setDoc(subjectRef, subject); - await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); -} - -export async function deleteSubject(subjectId) { - const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subjectId}`); - await deleteDoc(subjectRef); - await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); - } diff --git a/src/firebase/db/subjects.js b/src/firebase/db/subjects.js index 6eba559ac..55b1e5101 100644 --- a/src/firebase/db/subjects.js +++ b/src/firebase/db/subjects.js @@ -1 +1,45 @@ -export * from './subjects.cached'; +import { firestoreDB } from "../../main"; +import { + collection, + getDocs, + query, + orderBy, +} from 'firebase/firestore'; +import { doc, setDoc, deleteDoc } from 'firebase/firestore'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +export async function getSubjects(options = {}) { + return await withCache( + cacheKeys.subjects(), + { + ttlMs: CACHE_TTL_MS.SUBJECTS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.SUBJECTS] + }, + async () => { + const subjectsRef = collection(firestoreDB, "schools/tec.mx/subjects"); + const q = query(subjectsRef, orderBy("name")); + const docsSnap = await getDocs(q); + + if (!docsSnap.empty) { + return docsSnap.docs.map(doc => doc.data()); + } + + return null; + } + ); +} + +export async function addSubject(subject) { + const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subject.id}`); + await setDoc(subjectRef, subject); + await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); +} + +export async function deleteSubject(subjectId) { + const subjectRef = doc(firestoreDB, `schools/tec.mx/subjects/${subjectId}`); + await deleteDoc(subjectRef); + await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); + } diff --git a/src/firebase/db/users.cached.js b/src/firebase/db/users.cached.js deleted file mode 100644 index 0322fd213..000000000 --- a/src/firebase/db/users.cached.js +++ /dev/null @@ -1,1032 +0,0 @@ -import { firestoreDB } from "../../main"; -import { getAuth } from 'firebase/auth'; -import { - doc, - collection, - query, - where, - setDoc, - getDoc, - getDocs, - updateDoc, - serverTimestamp, - deleteField, - increment, - getFirestore, -} from 'firebase/firestore'; -import { getUserProfilePicture } from "../img/users"; -import * as XLSX from 'xlsx'; -import { writeBatch } from "firebase/firestore"; -import { invalidateCacheTags, withCache } from "../cache/cache"; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys, userTag } from "../cache/config"; - -const db = getFirestore(); -const MAE_DIRECTORY_ROLES = ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi', 'tec']; - - -function getEmailUsername(email) { - var atIndex = email.indexOf('@'); - if (atIndex !== -1) { - return email.slice(0, atIndex); - } - return null; -} - -function getCurrentDayKey() { - return ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][new Date().getDay()]; -} - -function sortUsersByClosestSchedule(data) { - const today = new Date().getDay(); - - data.sort((a, b) => { - const { day: dayA, startTime: startTimeA } = getClosestDayAndStartTime(a.weekSchedule); - const { day: dayB, startTime: startTimeB } = getClosestDayAndStartTime(b.weekSchedule); - - const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; - const daysOrdered = [...daysOfWeek.slice(today), ...daysOfWeek.slice(0, today)]; - - const dayIndexA = daysOrdered.indexOf(dayA); - const dayIndexB = daysOrdered.indexOf(dayB); - const dayComparison = (dayIndexA === -1 ? 1 : (dayIndexB === -1 ? -1 : dayIndexA - dayIndexB)); - if (dayComparison !== 0) return dayComparison; - - const startTimeComparison = (startTimeA === null ? 1 : (startTimeB === null ? -1 : startTimeA.localeCompare(startTimeB))); - if (startTimeComparison !== 0) return startTimeComparison; - - return a.name.localeCompare(b.name); - }); - - return data; -} - -async function fetchMaeDirectoryFresh() { - const usersRef = collection(firestoreDB, "users"); - const q = query(usersRef, where('role', 'in', MAE_DIRECTORY_ROLES)); - const querySnapshot = await getDocs(q); - - if (!querySnapshot) { - return null; - } - - let data = querySnapshot.docs.map(doc => doc.data()).filter(item => item.name); - - data = await Promise.all(data.map(async (item) => { - const profilePictureUrl = await getUserProfilePicture(item.email); - return { ...item, profilePictureUrl }; - })); - - return sortUsersByClosestSchedule(data); -} - -async function getMaeDirectory(options = {}) { - return await withCache( - cacheKeys.maeDirectory(), - { - ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] - }, - fetchMaeDirectoryFresh - ); -} - -async function invalidateUserCaches(userId, { includeActive = false, includeLeaderboard = false } = {}) { - const tags = [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, CACHE_TAGS.CURRENT_USER, CACHE_TAGS.MAES]; - - if (userId) { - tags.push(userTag(userId)); - } - if (includeActive) { - tags.push(CACHE_TAGS.ACTIVE_MAES); - } - if (includeLeaderboard) { - tags.push(CACHE_TAGS.LEADERBOARD); - } - - await invalidateCacheTags(tags); -} - -export async function createUser(userInfo) { - - userInfo.id = getEmailUsername(userInfo.email); - userInfo.uid = getEmailUsername(userInfo.email); - - userInfo.career = userInfo.major.id - userInfo.area = userInfo.major.school - - userInfo.name = userInfo.firstname.trim() + ' ' + userInfo.lastname.trim(); - - const userRef = doc(firestoreDB, "users", userInfo.uid); - const result = await setDoc(userRef, userInfo); - await invalidateUserCaches(userInfo.uid); - return result; -} - -export async function getUser(uid, options = {}) { - return await withCache( - cacheKeys.userById(uid), - { - ttlMs: CACHE_TTL_MS.USER, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, userTag(uid)] - }, - async () => { - const userRef = doc(firestoreDB, "users", uid); - const docSnap = await getDoc(userRef); - - if (docSnap.exists()) { - const data = docSnap.data(); - const profilePictureUrl = await getUserProfilePicture(data.email); - return { ...data, profilePictureUrl }; - } - - return null; - } - ); -} - -export async function getCurrentUser(options = {}) { - const auth = getAuth(); - if (auth.currentUser) { - const uid = getEmailUsername(auth.currentUser.email); - return await withCache( - cacheKeys.currentUser(uid), - { - ttlMs: CACHE_TTL_MS.CURRENT_USER, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.CURRENT_USER, userTag(uid)] - }, - async () => await getUser(uid, options) - ); - } - return null; -} - -// Función para obtener el día más cercano en la semana y la hora de inicio más temprana -export const getClosestDayAndStartTime = (schedules) => { - if (typeof schedules !== 'object' || schedules === null || Array.isArray(schedules)) { - console.error('Expected a map of schedules, but received:', schedules); - return { day: null, startTime: null }; - } - - const today = new Date().getDay(); // Día actual (0-6) donde 0 es domingo - const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; - - // Crear dos arrays, uno para los días futuros y otro para los pasados - const futureDays = daysOfWeek.slice(today); - const pastDays = daysOfWeek.slice(0, today); - - let closestDay = null; - let earliestStartTime = null; - - // Buscar primero entre los días futuros (desde hoy hasta el final de la semana) - futureDays.forEach(day => { - if (Array.isArray(schedules[day])) { - schedules[day].forEach(schedule => { - if (schedule.start) { - if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { - closestDay = day; - earliestStartTime = schedule.start; - } - } - }); - } - }); - - // Si no se encontró ningún día en el futuro, buscar en los días pasados (inicio de semana hasta hoy) - if (closestDay === null) { - pastDays.forEach(day => { - if (Array.isArray(schedules[day])) { - schedules[day].forEach(schedule => { - if (schedule.start) { - if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { - closestDay = day; - earliestStartTime = schedule.start; - } - } - }); - } - }); - } - - return { day: closestDay, startTime: earliestStartTime }; -}; - - -export async function getMaes(options = {}) { - return await getMaeDirectory(options); -} - -export async function getMaesNames(options = {}) { - return await getMaeDirectory(options); -} - - -export async function getUsersWithActiveSession(getProfilePicture = false, options = {}) { - try { - return await withCache( - cacheKeys.activeMaes(getProfilePicture ? 'with-photo' : 'basic'), - { - ttlMs: CACHE_TTL_MS.ACTIVE_MAES, - persist: false, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.ACTIVE_MAES] - }, - async () => { - const usersRef = collection(firestoreDB, "users"); - const q = query(usersRef, where('activeSession', '!=', null)); - const querySnapshot = await getDocs(q); - - if (!querySnapshot) { - return null; - } - - const fiveHoursAgoTimestampSeconds = Math.floor(Date.now() / 1000) - 18000; - const filteredDocs = querySnapshot.docs.filter((doc) => { - const data = doc.data(); - return data.activeSession?.startTime?.seconds > fiveHoursAgoTimestampSeconds; - }); - - return await Promise.all(filteredDocs.map(async (doc) => { - const data = doc.data(); - const profilePictureUrl = getProfilePicture ? await getUserProfilePicture(data.email) : null; - return { ...data, ...(profilePictureUrl ? { profilePictureUrl } : {}) }; - })); - } - ); - } catch (error) { - console.error('Error retrieving users:', error); - } -}; - -export async function updateUserInfo(userId, userInfo) { - userInfo['name'] = userInfo['firstname'].trim() + ' ' + userInfo['lastname'].trim() - const userRef = doc(firestoreDB, "users", userId); - const result = await updateDoc(userRef, userInfo); - await invalidateUserCaches(userId); - return result; -} - -export async function updateUserSubjects(userId, newSubjects) { - const userRef = doc(firestoreDB, "users", userId); - const result = await updateDoc(userRef, { - subjects: newSubjects - }); - await invalidateUserCaches(userId); - return result; -} - -export async function updateUserSchedule(userId, newSchedule) { - const userRef = doc(firestoreDB, "users", userId); - // Iterate over object keys - for (const day in newSchedule) { - // Check if the value is an empty array - if (Array.isArray(newSchedule[day]) && newSchedule[day].length === 0) { - // Delete the key with an empty array value - delete newSchedule[day]; - } - } - const result = await updateDoc(userRef, { - weekSchedule: newSchedule - }); - await invalidateUserCaches(userId); - return result; -} - -export async function getTodaysMae(options = {}) { - try { - return await withCache( - cacheKeys.maesToday(getCurrentDayKey()), - { - ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] - }, - async () => { - const users = await getMaeDirectory(options); - const currentDay = getCurrentDayKey(); - - return (users ?? []) - .filter((user) => user.weekSchedule && user.weekSchedule[currentDay]) - .sort((a, b) => { - const aStartTime = a.weekSchedule[currentDay][0]?.start; - const bStartTime = b.weekSchedule[currentDay][0]?.start; - const aTime = aStartTime ? new Date(`1970-01-01T${aStartTime}:00Z`) : new Date(); - const bTime = bStartTime ? new Date(`1970-01-01T${bStartTime}:00Z`) : new Date(); - return aTime - bTime; - }); - } - ); - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -export async function startActiveSession(userId, userInfo, location) { - try { - const userRef = doc(firestoreDB, "users", userId); - const result = await updateDoc(userRef, { - activeSession: { - peerInfo: userInfo, - location, - status: 'PENDING', - startTime: serverTimestamp(), - } - }); - await invalidateUserCaches(userId, { includeActive: true }); - return result; - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -export async function stopActiveSession(userId) { - try { - const userRef = doc(firestoreDB, "users", userId); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - throw new Error("User not found"); - } - - // Gets start time from current Active Session - const userData = userDoc.data(); - const startTime = userData.activeSession?.startTime?.toDate(); - - if (!startTime) { - throw new Error("Active session start time not found"); - } - - // Calculates and adds the duration of the current session to the total time - const currentTime = new Date(); - const differenceInMinutes = Math.floor((currentTime - startTime) / (1000 * 60)); - - if (differenceInMinutes > 310) { - await updateDoc(userRef, { - activeSession: deleteField() - }); - await invalidateUserCaches(userId, { includeActive: true }); - return { timeLimitExceded: true, activeSessionDeleted: false, differenceInMinutes } - } - - const totalTime = (userData.totalTime || 0) + differenceInMinutes; - - // Updates the total time and stops current session - await updateDoc(userRef, { - totalTime: totalTime, - activeSession: deleteField() - }); - await invalidateUserCaches(userId, { includeActive: true, includeLeaderboard: true }); - - return { totalTime, differenceInMinutes, activeSessionDeleted: true }; - } catch (error) { - return { activeSessionDeleted: false }; - } -} - -export async function incrementTotalTime(userId, time) { - const userRef = doc(firestoreDB, "users", userId); - - await updateDoc(userRef, { - totalTime: increment(time*60) - }); - await invalidateUserCaches(userId, { includeLeaderboard: true }); -} - - -export async function updateUserProfilePicture(userId, photoURL) { - try { - const userRef = doc(firestoreDB, 'users', userId); - - await updateDoc(userRef, { - photoURL: photoURL - }); - await invalidateUserCaches(userId); - - } catch (error) { - console.error('Error updating user profile picture: ', error); - throw error; - } -} - -/** - * Clears the content of the weekSchedule field for users with specific roles (admin, coordi, mae), - * but keeps the field as an empty object. - * - * @returns {Promise} - A promise that resolves when all eligible weekSchedules are cleared. - */ -export async function clearAllUsersWeekSchedule() { - try { - - const usersRef = collection(firestoreDB, "users"); - - const querySnapshot = await getDocs(usersRef); - - - const eligibleRoles = ['admin', 'coordi', 'mae','tec','publi']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - weekSchedule: {} - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - - console.log("Week schedule content has been successfully cleared for eligible users."); - } catch (error) { - console.error("Error clearing weekSchedule content for eligible users: ", error); - throw error; - } -} - -export async function checkAndUpdateUserRole(file = null) { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - if (file) { - // Leer y procesar el archivo Excel acuerdate que empieza a contar desde 0 - const reader = new FileReader(); - reader.onload = async (event) => { - try { - const data = new Uint8Array(event.target.result); - const workbook = XLSX.read(data, { type: 'array' }); - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - // console.log(sheet) - const excelData = XLSX.utils.sheet_to_json(sheet, { header: 0 }); - console.log(excelData) - // Convertimos las matrículas del Excel a correos en formato lowercase@tec.mx - const emailsFromExcel = excelData - .map(row => row["Matrícula"]?.toLowerCase() + "@tec.mx") - - console.log(emailsFromExcel); - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - const eligibleRoles = ['mae', 'coordi', 'publi', 'tec', 'admin']; - - const userEmail = userData.email?.toLowerCase(); - const userMatricula = userData.matricula?.toLowerCase(); - - if (eligibleRoles.includes(userData.role)) { - if (!emailsFromExcel.includes(userEmail)) { - return updateDoc(userRef, { role: "exmae" }); - } - } else { - // Si el usuario no tiene un rol elegible, se le asigna "mae" con estado "becario" - if (emailsFromExcel.includes(userEmail)) { - return updateUserToMae({ - matricula: userMatricula, - role: "mae", - status: "becario", - point: 0, - useCoins: 0, - }); - } - } - - return Promise.resolve(); - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - //console.log("Roles actualizados con base en el archivo Excel."); - } catch (error) { - console.error("Error al procesar el archivo Excel:", error); - throw error; - } - }; - - reader.readAsArrayBuffer(file); - } else { - // Si no hay archivo, ejecutamos la lógica normal - const eligibleRoles = ['mae', 'coordi', 'publi','tec']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - if (eligibleRoles.includes(userData.role)) { - const isWeekScheduleEmpty = Object.values(userData.weekSchedule).every(day => day.length === 0); - const isTotalTimeEquals0 = userData.totalTime == 0; - const hasNoSubjects = !userData.subjects || userData.subjects.length === 0; - if (isWeekScheduleEmpty && isTotalTimeEquals0 && hasNoSubjects) { - return updateDoc(userRef, { role: "exmae" }); - } - } - - return Promise.resolve(); - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - console.log("Roles actualizados con base en weekSchedule, totalTime, y subjects."); - } - } catch (error) { - console.error("Error actualizando roles de los usuarios: ", error); - throw error; - } -} - -export async function updateUserToMae(data) { - const { role, matricula, status } = data; - const badges = [ - { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, - { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, - { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, - { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, - { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, - { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, - { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, - { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, - { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, - { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, - { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, - { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, - { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, - { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, - { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, - { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, - { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, - { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } - ]; - - - - const background = [ - { "id": "1", "image_url": "/assets/back/1.svg", "bought": true, "price": 0 }, - { "id": "2", "image_url": "/assets/back/2.svg", "bought": false, "price": 25 }, - { "id": "3", "image_url": "/assets/back/3.svg", "bought": false, "price": 25}, - { "id": "4", "image_url": "/assets/back/4.svg", "bought": false, "price": 25 }, - { "id": "5", "image_url": "/assets/back/5.svg", "bought": false, "price": 50 }, - { "id": "6", "image_url": "/assets/back/6.svg", "bought": false, "price": 50 }, - { "id": "7", "image_url": "/assets/back/7.svg", "bought": false, "price": 75 }, - { "id": "8", "image_url": "/assets/back/8.svg", "bought": false, "price": 100 }, - ]; - - if (!role || !matricula || !status) { - throw new Error("role, matricula, and status are required fields."); - } - - try { - const usersRef = collection(firestoreDB, "users"); - const userQuery = query(usersRef, where("email", "==", `${matricula.toLowerCase()}@tec.mx`)); - const querySnapshot = await getDocs(userQuery); - - if (querySnapshot.empty) { - console.log("No user found with the given matricula."); - return; - } - - // Procesar cada usuario encontrado - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (userData.role === 'user' || userData.status === 'estudiante') { - return updateDoc(userRef, { - role: role.value, - status: status.value, - weekSchedule: {}, - subjects: [], - totalTime: 0, - badges: badges, - points: 0, - useCoins: 0, - background: background, - }); - } else { - - return updateDoc(userRef, { - role: role.value, - status: status.value - }); - } - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - console.log("Usuarios actualizados exitosamente."); - } catch (error) { - console.error("Error al actualizar los usuarios: ", error); - } -} - -export const saveScheduleSubjectsExperience = async () => { - try { - const usersRef = collection(db, 'users'); - const usersSnap = await getDocs(usersRef); - - if (usersSnap.empty) { - console.error("No se encontraron usuarios en la tabla 'users'."); - return; - } - - const rolesPermitidos = ['admin', 'publi', 'mae', 'coordi', 'tec']; - - const updatePromises = []; - usersSnap.forEach(async (userDoc) => { - const user = userDoc.data(); - - if (!rolesPermitidos.includes(user.role)) { - return; - } - - let puntos = 0; - - if (user.subjects && user.subjects.length > 0) { - puntos += 15; - } else { - puntos -= 30; - await updateUserAchievementBadge(user.uid, "18"); - } - - if (user.weekSchedule && Object.keys(user.weekSchedule).length > 0) { - puntos += 100; - } else { - puntos -= 500; - await updateUserAchievementBadge(user.uid, "18"); - } - - const userRef = doc(db, 'users', userDoc.id); - updatePromises.push( - updateDoc(userRef, { - points: (user.points || 0) + puntos - }) - ); - }); - - await Promise.all(updatePromises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - } catch (error) { - console.error("Error al guardar la experiencia:", error); - } -}; - - -export async function updatePoints(uid, newPoints) { - const userRef = doc(db, 'users', uid); - const userSnap = await getDoc(userRef); - - if (!userSnap.exists()) { - console.log(`Usuario con uid ${uid} no encontrado.`); - return []; - } - - const user = userSnap.data(); - const updatedPoints = (user.points || 0) + newPoints; - - await updateDoc(userRef, { points: updatedPoints }); - if (newPoints < 0) { - await updateUserAchievementBadge(uid, "18"); - } - - await invalidateUserCaches(uid, { includeLeaderboard: true }); - return [{ id: uid, ...user, points: updatedPoints }]; -} - -export async function getExperience(options = {}) { - return await withCache( - cacheKeys.leaderboard(), - { - ttlMs: CACHE_TTL_MS.LEADERBOARD, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.USERS, CACHE_TAGS.LEADERBOARD] - }, - async () => { - const data = await getMaeDirectory(options); - return (data ?? []).slice().sort((a, b) => (b.points || 0) - (a.points || 0)); - } - ); -} - -// Funcion especial si mas adelante quieren agregar logros -export async function addBadgesToEligibleUsers() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const badges = [ - { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, - { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, - { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, - { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, - { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, - { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, - { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, - { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, - { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, - { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, - { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, - { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, - { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, - { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, - { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, - { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, - { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, - { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } - ]; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - badges: badges - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - - console.log("Badges have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding badges to eligible users: ", error); - throw error; - } -} - - -// actualizar le achieved del usuario -export async function updateUserAchievementBadge(uid, badgeId) { - try { - - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - console.error("Usuario no encontrado"); - return; - } - - const userData = userDoc.data(); - const badges = userData.badges || []; - - const updatedBadges = badges.map((badge) => { - if (badge.id === badgeId) { - return { ...badge, achieved: true }; - } - return badge; - }); - - await updateDoc(userRef, { - badges: updatedBadges - }); - await invalidateUserCaches(uid, { includeLeaderboard: true }); - - // console.log(`El logro con id ${badgeId} se ha actualizado correctamente para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el logro del usuario:", error); - throw error; - } -} - - -// Contador de badges -export async function countAchievedBadges(uid) { - try { - const user = await getUser(uid); - - if (!user) { - console.error("Usuario no encontrado"); - return 0; - } - - const badges = user.badges || []; - return badges.reduce((count, badge) => count + (badge.achieved ? 1 : 0), 0); - } catch (error) { - console.error("Error al contar los logros alcanzados:", error); - throw error; - } -} - -// A??adir nuevos backgrounds a los usuarios sin borrar los existentes -export async function addBackgroundUsers() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const newBackgrounds = [ - { id: '8', image_url: '/assets/back/8.svg', bought: false, price: 100 }, - // Aquí puedes agregar más fondos nuevos... - ]; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - const currentBackgrounds = userData.background || []; - - // Crear un mapa de fondos existentes (para evitar duplicados) - const backgroundMap = new Map(currentBackgrounds.map(bg => [bg.id, bg])); - - // Añadir los nuevos fondos solo si no existen ya - newBackgrounds.forEach(bg => { - if (!backgroundMap.has(bg.id)) { - backgroundMap.set(bg.id, bg); - } - }); - - const mergedBackgrounds = Array.from(backgroundMap.values()); - - return updateDoc(userRef, { - background: mergedBackgrounds, - myBackground: userData.myBackground || "/assets/back/1.svg", // conservar el que ya tiene - useCoins: userData.useCoins || 0, // conservar las monedas que ya tiene - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - - console.log("Backgrounds have been merged successfully for eligible users."); - } catch (error) { - console.error("Error merging backgrounds for eligible users: ", error); - throw error; -} -} - - - -// actualizar le achieved del usuario -export async function updateUserBackground(uid, backId, coins, userCoins) { - try { - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); - - if (!userDoc.exists()) { - console.error("Usuario no encontrado"); - return; - } - - const userData = userDoc.data(); - const background = userData.background || []; - - const updatedBackground = background.map((back) => { - if (back.id === backId) { - return { ...back, bought: true }; - } - return back; - }); - - await updateDoc(userRef, { - background: updatedBackground, - useCoins: userCoins + coins - }); - await invalidateUserCaches(uid, { includeLeaderboard: true }); - - console.log(`El fondo con id ${backId} se ha actualizado correctamente para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el fondo del usuario:", error); - throw error; - } -} - -// Actualizar fondo -export async function updateUserBackgroundImage(uid, backgroundUrl) { - try { - const userRef = doc(firestoreDB, "users", uid); - await updateDoc(userRef, { - myBackground: backgroundUrl - }); - await invalidateUserCaches(uid); - console.log(`El fondo se ha actualizado a ${backgroundUrl} para el usuario ${uid}.`); - } catch (error) { - console.error("Error al actualizar el fondo del usuario:", error); - throw error; - } -} - - -export async function getTotalMaes(options = {}) { - try { - const maes = await getMaeDirectory(options); - return (maes ?? []).filter((user) => user.uid !== 'jackpot').length; - } catch (error) { - console.error("Error al obtener el total de MAEs: ", error); - throw error; - } -} - - - -// Añadir nuevas variables -export async function addExtraVariables() { - try { - const usersRef = collection(firestoreDB, "users"); - const querySnapshot = await getDocs(usersRef); - - const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; - - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - const userData = doc.data(); - - if (eligibleRoles.includes(userData.role)) { - return updateDoc(userRef, { - asesoriasGrupales: 0, - }); - } else { - return Promise.resolve(); - } - }); - - await Promise.all(promises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - - console.log("Background have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding background to eligible users: ", error); - throw error; - } -} - - - -export async function clearUsersData() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "users")); - const rolesToUpdate = ["admin", "coordi", "mae", "tec", "publi"]; - - const updatePromises = []; - - querySnapshot.forEach(docSnapshot => { - const userData = docSnapshot.data(); - if (rolesToUpdate.includes(userData.role)) { - const userRef = doc(firestoreDB, "users", docSnapshot.id); - updatePromises.push(updateDoc(userRef, { - useCoins: userData.useCoins - userData.points, - subjects: [], - totalTime: 0, - points: 0 - })); - } - }); - - await Promise.all(updatePromises); - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - console.log("Usuarios actualizados correctamente."); - } catch (error) { - console.error("Error al actualizar usuarios: ", error); - throw error; - } -} - - -export async function resetAllUsersTotalTimeAndPoints({ dryRun = false, batchSize = 450 } = {}) { - const usersSnap = await getDocs(collection(firestoreDB, "users")); - if (usersSnap.empty) return { scanned: 0, updated: 0 }; - - const docs = usersSnap.docs; - let updated = 0; - - if (dryRun) { - return { scanned: docs.length, updated: 0 }; - } - - for (let i = 0; i < docs.length; i += batchSize) { - const chunk = docs.slice(i, i + batchSize); - const batch = writeBatch(firestoreDB); - - chunk.forEach((d) => { - batch.update(d.ref, { totalTime: 0, points: 0 }); - }); - - await batch.commit(); - updated += chunk.length; - console.log(`✅ Restablecimiento en progreso: ${updated}/${docs.length}`); - } - - await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); - console.log(`Reset complete. Se restablecieron totalTime y points para ${updated} usuarios.`); - return { scanned: docs.length, updated }; -} diff --git a/src/firebase/db/users.js b/src/firebase/db/users.js index 403f09dce..0322fd213 100644 --- a/src/firebase/db/users.js +++ b/src/firebase/db/users.js @@ -1 +1,1032 @@ -export * from './users.cached'; +import { firestoreDB } from "../../main"; +import { getAuth } from 'firebase/auth'; +import { + doc, + collection, + query, + where, + setDoc, + getDoc, + getDocs, + updateDoc, + serverTimestamp, + deleteField, + increment, + getFirestore, +} from 'firebase/firestore'; +import { getUserProfilePicture } from "../img/users"; +import * as XLSX from 'xlsx'; +import { writeBatch } from "firebase/firestore"; +import { invalidateCacheTags, withCache } from "../cache/cache"; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys, userTag } from "../cache/config"; + +const db = getFirestore(); +const MAE_DIRECTORY_ROLES = ['mae', 'coordi', 'admin', 'subjectCoordi', 'publi', 'tec']; + + +function getEmailUsername(email) { + var atIndex = email.indexOf('@'); + if (atIndex !== -1) { + return email.slice(0, atIndex); + } + return null; +} + +function getCurrentDayKey() { + return ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][new Date().getDay()]; +} + +function sortUsersByClosestSchedule(data) { + const today = new Date().getDay(); + + data.sort((a, b) => { + const { day: dayA, startTime: startTimeA } = getClosestDayAndStartTime(a.weekSchedule); + const { day: dayB, startTime: startTimeB } = getClosestDayAndStartTime(b.weekSchedule); + + const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + const daysOrdered = [...daysOfWeek.slice(today), ...daysOfWeek.slice(0, today)]; + + const dayIndexA = daysOrdered.indexOf(dayA); + const dayIndexB = daysOrdered.indexOf(dayB); + const dayComparison = (dayIndexA === -1 ? 1 : (dayIndexB === -1 ? -1 : dayIndexA - dayIndexB)); + if (dayComparison !== 0) return dayComparison; + + const startTimeComparison = (startTimeA === null ? 1 : (startTimeB === null ? -1 : startTimeA.localeCompare(startTimeB))); + if (startTimeComparison !== 0) return startTimeComparison; + + return a.name.localeCompare(b.name); + }); + + return data; +} + +async function fetchMaeDirectoryFresh() { + const usersRef = collection(firestoreDB, "users"); + const q = query(usersRef, where('role', 'in', MAE_DIRECTORY_ROLES)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot) { + return null; + } + + let data = querySnapshot.docs.map(doc => doc.data()).filter(item => item.name); + + data = await Promise.all(data.map(async (item) => { + const profilePictureUrl = await getUserProfilePicture(item.email); + return { ...item, profilePictureUrl }; + })); + + return sortUsersByClosestSchedule(data); +} + +async function getMaeDirectory(options = {}) { + return await withCache( + cacheKeys.maeDirectory(), + { + ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] + }, + fetchMaeDirectoryFresh + ); +} + +async function invalidateUserCaches(userId, { includeActive = false, includeLeaderboard = false } = {}) { + const tags = [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, CACHE_TAGS.CURRENT_USER, CACHE_TAGS.MAES]; + + if (userId) { + tags.push(userTag(userId)); + } + if (includeActive) { + tags.push(CACHE_TAGS.ACTIVE_MAES); + } + if (includeLeaderboard) { + tags.push(CACHE_TAGS.LEADERBOARD); + } + + await invalidateCacheTags(tags); +} + +export async function createUser(userInfo) { + + userInfo.id = getEmailUsername(userInfo.email); + userInfo.uid = getEmailUsername(userInfo.email); + + userInfo.career = userInfo.major.id + userInfo.area = userInfo.major.school + + userInfo.name = userInfo.firstname.trim() + ' ' + userInfo.lastname.trim(); + + const userRef = doc(firestoreDB, "users", userInfo.uid); + const result = await setDoc(userRef, userInfo); + await invalidateUserCaches(userInfo.uid); + return result; +} + +export async function getUser(uid, options = {}) { + return await withCache( + cacheKeys.userById(uid), + { + ttlMs: CACHE_TTL_MS.USER, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.USER_DETAILS, userTag(uid)] + }, + async () => { + const userRef = doc(firestoreDB, "users", uid); + const docSnap = await getDoc(userRef); + + if (docSnap.exists()) { + const data = docSnap.data(); + const profilePictureUrl = await getUserProfilePicture(data.email); + return { ...data, profilePictureUrl }; + } + + return null; + } + ); +} + +export async function getCurrentUser(options = {}) { + const auth = getAuth(); + if (auth.currentUser) { + const uid = getEmailUsername(auth.currentUser.email); + return await withCache( + cacheKeys.currentUser(uid), + { + ttlMs: CACHE_TTL_MS.CURRENT_USER, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.CURRENT_USER, userTag(uid)] + }, + async () => await getUser(uid, options) + ); + } + return null; +} + +// Función para obtener el día más cercano en la semana y la hora de inicio más temprana +export const getClosestDayAndStartTime = (schedules) => { + if (typeof schedules !== 'object' || schedules === null || Array.isArray(schedules)) { + console.error('Expected a map of schedules, but received:', schedules); + return { day: null, startTime: null }; + } + + const today = new Date().getDay(); // Día actual (0-6) donde 0 es domingo + const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + + // Crear dos arrays, uno para los días futuros y otro para los pasados + const futureDays = daysOfWeek.slice(today); + const pastDays = daysOfWeek.slice(0, today); + + let closestDay = null; + let earliestStartTime = null; + + // Buscar primero entre los días futuros (desde hoy hasta el final de la semana) + futureDays.forEach(day => { + if (Array.isArray(schedules[day])) { + schedules[day].forEach(schedule => { + if (schedule.start) { + if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { + closestDay = day; + earliestStartTime = schedule.start; + } + } + }); + } + }); + + // Si no se encontró ningún día en el futuro, buscar en los días pasados (inicio de semana hasta hoy) + if (closestDay === null) { + pastDays.forEach(day => { + if (Array.isArray(schedules[day])) { + schedules[day].forEach(schedule => { + if (schedule.start) { + if (closestDay === null || (earliestStartTime === null || schedule.start < earliestStartTime)) { + closestDay = day; + earliestStartTime = schedule.start; + } + } + }); + } + }); + } + + return { day: closestDay, startTime: earliestStartTime }; +}; + + +export async function getMaes(options = {}) { + return await getMaeDirectory(options); +} + +export async function getMaesNames(options = {}) { + return await getMaeDirectory(options); +} + + +export async function getUsersWithActiveSession(getProfilePicture = false, options = {}) { + try { + return await withCache( + cacheKeys.activeMaes(getProfilePicture ? 'with-photo' : 'basic'), + { + ttlMs: CACHE_TTL_MS.ACTIVE_MAES, + persist: false, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.ACTIVE_MAES] + }, + async () => { + const usersRef = collection(firestoreDB, "users"); + const q = query(usersRef, where('activeSession', '!=', null)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot) { + return null; + } + + const fiveHoursAgoTimestampSeconds = Math.floor(Date.now() / 1000) - 18000; + const filteredDocs = querySnapshot.docs.filter((doc) => { + const data = doc.data(); + return data.activeSession?.startTime?.seconds > fiveHoursAgoTimestampSeconds; + }); + + return await Promise.all(filteredDocs.map(async (doc) => { + const data = doc.data(); + const profilePictureUrl = getProfilePicture ? await getUserProfilePicture(data.email) : null; + return { ...data, ...(profilePictureUrl ? { profilePictureUrl } : {}) }; + })); + } + ); + } catch (error) { + console.error('Error retrieving users:', error); + } +}; + +export async function updateUserInfo(userId, userInfo) { + userInfo['name'] = userInfo['firstname'].trim() + ' ' + userInfo['lastname'].trim() + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, userInfo); + await invalidateUserCaches(userId); + return result; +} + +export async function updateUserSubjects(userId, newSubjects) { + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, { + subjects: newSubjects + }); + await invalidateUserCaches(userId); + return result; +} + +export async function updateUserSchedule(userId, newSchedule) { + const userRef = doc(firestoreDB, "users", userId); + // Iterate over object keys + for (const day in newSchedule) { + // Check if the value is an empty array + if (Array.isArray(newSchedule[day]) && newSchedule[day].length === 0) { + // Delete the key with an empty array value + delete newSchedule[day]; + } + } + const result = await updateDoc(userRef, { + weekSchedule: newSchedule + }); + await invalidateUserCaches(userId); + return result; +} + +export async function getTodaysMae(options = {}) { + try { + return await withCache( + cacheKeys.maesToday(getCurrentDayKey()), + { + ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] + }, + async () => { + const users = await getMaeDirectory(options); + const currentDay = getCurrentDayKey(); + + return (users ?? []) + .filter((user) => user.weekSchedule && user.weekSchedule[currentDay]) + .sort((a, b) => { + const aStartTime = a.weekSchedule[currentDay][0]?.start; + const bStartTime = b.weekSchedule[currentDay][0]?.start; + const aTime = aStartTime ? new Date(`1970-01-01T${aStartTime}:00Z`) : new Date(); + const bTime = bStartTime ? new Date(`1970-01-01T${bStartTime}:00Z`) : new Date(); + return aTime - bTime; + }); + } + ); + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +export async function startActiveSession(userId, userInfo, location) { + try { + const userRef = doc(firestoreDB, "users", userId); + const result = await updateDoc(userRef, { + activeSession: { + peerInfo: userInfo, + location, + status: 'PENDING', + startTime: serverTimestamp(), + } + }); + await invalidateUserCaches(userId, { includeActive: true }); + return result; + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +export async function stopActiveSession(userId) { + try { + const userRef = doc(firestoreDB, "users", userId); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + throw new Error("User not found"); + } + + // Gets start time from current Active Session + const userData = userDoc.data(); + const startTime = userData.activeSession?.startTime?.toDate(); + + if (!startTime) { + throw new Error("Active session start time not found"); + } + + // Calculates and adds the duration of the current session to the total time + const currentTime = new Date(); + const differenceInMinutes = Math.floor((currentTime - startTime) / (1000 * 60)); + + if (differenceInMinutes > 310) { + await updateDoc(userRef, { + activeSession: deleteField() + }); + await invalidateUserCaches(userId, { includeActive: true }); + return { timeLimitExceded: true, activeSessionDeleted: false, differenceInMinutes } + } + + const totalTime = (userData.totalTime || 0) + differenceInMinutes; + + // Updates the total time and stops current session + await updateDoc(userRef, { + totalTime: totalTime, + activeSession: deleteField() + }); + await invalidateUserCaches(userId, { includeActive: true, includeLeaderboard: true }); + + return { totalTime, differenceInMinutes, activeSessionDeleted: true }; + } catch (error) { + return { activeSessionDeleted: false }; + } +} + +export async function incrementTotalTime(userId, time) { + const userRef = doc(firestoreDB, "users", userId); + + await updateDoc(userRef, { + totalTime: increment(time*60) + }); + await invalidateUserCaches(userId, { includeLeaderboard: true }); +} + + +export async function updateUserProfilePicture(userId, photoURL) { + try { + const userRef = doc(firestoreDB, 'users', userId); + + await updateDoc(userRef, { + photoURL: photoURL + }); + await invalidateUserCaches(userId); + + } catch (error) { + console.error('Error updating user profile picture: ', error); + throw error; + } +} + +/** + * Clears the content of the weekSchedule field for users with specific roles (admin, coordi, mae), + * but keeps the field as an empty object. + * + * @returns {Promise} - A promise that resolves when all eligible weekSchedules are cleared. + */ +export async function clearAllUsersWeekSchedule() { + try { + + const usersRef = collection(firestoreDB, "users"); + + const querySnapshot = await getDocs(usersRef); + + + const eligibleRoles = ['admin', 'coordi', 'mae','tec','publi']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + weekSchedule: {} + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Week schedule content has been successfully cleared for eligible users."); + } catch (error) { + console.error("Error clearing weekSchedule content for eligible users: ", error); + throw error; + } +} + +export async function checkAndUpdateUserRole(file = null) { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + if (file) { + // Leer y procesar el archivo Excel acuerdate que empieza a contar desde 0 + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const data = new Uint8Array(event.target.result); + const workbook = XLSX.read(data, { type: 'array' }); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + // console.log(sheet) + const excelData = XLSX.utils.sheet_to_json(sheet, { header: 0 }); + console.log(excelData) + // Convertimos las matrículas del Excel a correos en formato lowercase@tec.mx + const emailsFromExcel = excelData + .map(row => row["Matrícula"]?.toLowerCase() + "@tec.mx") + + console.log(emailsFromExcel); + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + const eligibleRoles = ['mae', 'coordi', 'publi', 'tec', 'admin']; + + const userEmail = userData.email?.toLowerCase(); + const userMatricula = userData.matricula?.toLowerCase(); + + if (eligibleRoles.includes(userData.role)) { + if (!emailsFromExcel.includes(userEmail)) { + return updateDoc(userRef, { role: "exmae" }); + } + } else { + // Si el usuario no tiene un rol elegible, se le asigna "mae" con estado "becario" + if (emailsFromExcel.includes(userEmail)) { + return updateUserToMae({ + matricula: userMatricula, + role: "mae", + status: "becario", + point: 0, + useCoins: 0, + }); + } + } + + return Promise.resolve(); + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + //console.log("Roles actualizados con base en el archivo Excel."); + } catch (error) { + console.error("Error al procesar el archivo Excel:", error); + throw error; + } + }; + + reader.readAsArrayBuffer(file); + } else { + // Si no hay archivo, ejecutamos la lógica normal + const eligibleRoles = ['mae', 'coordi', 'publi','tec']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + if (eligibleRoles.includes(userData.role)) { + const isWeekScheduleEmpty = Object.values(userData.weekSchedule).every(day => day.length === 0); + const isTotalTimeEquals0 = userData.totalTime == 0; + const hasNoSubjects = !userData.subjects || userData.subjects.length === 0; + if (isWeekScheduleEmpty && isTotalTimeEquals0 && hasNoSubjects) { + return updateDoc(userRef, { role: "exmae" }); + } + } + + return Promise.resolve(); + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Roles actualizados con base en weekSchedule, totalTime, y subjects."); + } + } catch (error) { + console.error("Error actualizando roles de los usuarios: ", error); + throw error; + } +} + +export async function updateUserToMae(data) { + const { role, matricula, status } = data; + const badges = [ + { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, + { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, + { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, + { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, + { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, + { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, + { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, + { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, + { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, + { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, + { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, + { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, + { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, + { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, + { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, + { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, + { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, + { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } + ]; + + + + const background = [ + { "id": "1", "image_url": "/assets/back/1.svg", "bought": true, "price": 0 }, + { "id": "2", "image_url": "/assets/back/2.svg", "bought": false, "price": 25 }, + { "id": "3", "image_url": "/assets/back/3.svg", "bought": false, "price": 25}, + { "id": "4", "image_url": "/assets/back/4.svg", "bought": false, "price": 25 }, + { "id": "5", "image_url": "/assets/back/5.svg", "bought": false, "price": 50 }, + { "id": "6", "image_url": "/assets/back/6.svg", "bought": false, "price": 50 }, + { "id": "7", "image_url": "/assets/back/7.svg", "bought": false, "price": 75 }, + { "id": "8", "image_url": "/assets/back/8.svg", "bought": false, "price": 100 }, + ]; + + if (!role || !matricula || !status) { + throw new Error("role, matricula, and status are required fields."); + } + + try { + const usersRef = collection(firestoreDB, "users"); + const userQuery = query(usersRef, where("email", "==", `${matricula.toLowerCase()}@tec.mx`)); + const querySnapshot = await getDocs(userQuery); + + if (querySnapshot.empty) { + console.log("No user found with the given matricula."); + return; + } + + // Procesar cada usuario encontrado + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (userData.role === 'user' || userData.status === 'estudiante') { + return updateDoc(userRef, { + role: role.value, + status: status.value, + weekSchedule: {}, + subjects: [], + totalTime: 0, + badges: badges, + points: 0, + useCoins: 0, + background: background, + }); + } else { + + return updateDoc(userRef, { + role: role.value, + status: status.value + }); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Usuarios actualizados exitosamente."); + } catch (error) { + console.error("Error al actualizar los usuarios: ", error); + } +} + +export const saveScheduleSubjectsExperience = async () => { + try { + const usersRef = collection(db, 'users'); + const usersSnap = await getDocs(usersRef); + + if (usersSnap.empty) { + console.error("No se encontraron usuarios en la tabla 'users'."); + return; + } + + const rolesPermitidos = ['admin', 'publi', 'mae', 'coordi', 'tec']; + + const updatePromises = []; + usersSnap.forEach(async (userDoc) => { + const user = userDoc.data(); + + if (!rolesPermitidos.includes(user.role)) { + return; + } + + let puntos = 0; + + if (user.subjects && user.subjects.length > 0) { + puntos += 15; + } else { + puntos -= 30; + await updateUserAchievementBadge(user.uid, "18"); + } + + if (user.weekSchedule && Object.keys(user.weekSchedule).length > 0) { + puntos += 100; + } else { + puntos -= 500; + await updateUserAchievementBadge(user.uid, "18"); + } + + const userRef = doc(db, 'users', userDoc.id); + updatePromises.push( + updateDoc(userRef, { + points: (user.points || 0) + puntos + }) + ); + }); + + await Promise.all(updatePromises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + } catch (error) { + console.error("Error al guardar la experiencia:", error); + } +}; + + +export async function updatePoints(uid, newPoints) { + const userRef = doc(db, 'users', uid); + const userSnap = await getDoc(userRef); + + if (!userSnap.exists()) { + console.log(`Usuario con uid ${uid} no encontrado.`); + return []; + } + + const user = userSnap.data(); + const updatedPoints = (user.points || 0) + newPoints; + + await updateDoc(userRef, { points: updatedPoints }); + if (newPoints < 0) { + await updateUserAchievementBadge(uid, "18"); + } + + await invalidateUserCaches(uid, { includeLeaderboard: true }); + return [{ id: uid, ...user, points: updatedPoints }]; +} + +export async function getExperience(options = {}) { + return await withCache( + cacheKeys.leaderboard(), + { + ttlMs: CACHE_TTL_MS.LEADERBOARD, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.USERS, CACHE_TAGS.LEADERBOARD] + }, + async () => { + const data = await getMaeDirectory(options); + return (data ?? []).slice().sort((a, b) => (b.points || 0) - (a.points || 0)); + } + ); +} + +// Funcion especial si mas adelante quieren agregar logros +export async function addBadgesToEligibleUsers() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const badges = [ + { "id": "1", "name": "Mi primera asesoría", "description": "Da tu primera asesoría", "image_url": "/assets/badges/1.svg", "achieved": false }, + { "id": "2", "name": "MAE aprendiz", "description": "Da 10 asesorías", "image_url": "/assets/badges/2.svg", "achieved": false }, + { "id": "3", "name": "MAE en ascenso", "description": "Da 30 asesorías", "image_url": "/assets/badges/3.svg", "achieved": false }, + { "id": "4", "name": "MAE destacado", "description": "Da 50 asesorías", "image_url": "/assets/badges/4.svg", "achieved": false }, + { "id": "5", "name": "Super MAE", "description": "Da 100 asesorías", "image_url": "/assets/badges/5.svg", "achieved": false }, + { "id": "6", "name": "Leyenda MAE", "description": "Da 200 asesorías", "image_url": "/assets/badges/6.svg", "achieved": false }, + { "id": "7", "name": "MAE de MAEs", "description": "Da 500 asesorías", "image_url": "/assets/badges/7.svg", "achieved": false }, + { "id": "8", "name": "Cambio de look", "description": "Añade una foto de perfil", "image_url": "/assets/badges/8.svg", "achieved": false }, + { "id": "9", "name": "Trabajo bien hecho", "description": "Completa 80 horas", "image_url": "/assets/badges/9.svg", "achieved": false }, + { "id": "10", "name": "Siempre a tiempo", "description": "Obtén asistencia perfecta durante 1 periodo", "image_url": "/assets/badges/10.svg", "achieved": false }, + { "id": "11", "name": "Top MAE", "description": "Se #1 en el leaderboard", "image_url": "/assets/badges/11.svg", "achieved": false }, + { "id": "12", "name": "MAE", "description": "Obtén el rol de MAE", "image_url": "/assets/badges/12.svg", "achieved": false }, + { "id": "13", "name": "Coordi", "description": "Obtén el rol de coordi", "image_url": "/assets/badges/13.svg", "achieved": false }, + { "id": "14", "name": "Tecnológico", "description": "Obtén el rol de tecnología", "image_url": "/assets/badges/14.svg", "achieved": false }, + { "id": "15", "name": "Publicista", "description": "Obtén el rol de publicidad", "image_url": "/assets/badges/15.svg", "achieved": false }, + { "id": "16", "name": "Especialista", "description": "Mete 3 materias top", "image_url": "/assets/badges/16.svg", "achieved": false }, + { "id": "17", "name": "Trabajo de campo", "description": "Da 5 asesorías de materias top", "image_url": "/assets/badges/17.svg", "achieved": false }, + { "id": "18", "name": "Ups...", "description": "Pierde puntos de experiencia una vez", "image_url": "/assets/badges/18.svg", "achieved": false } + ]; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + badges: badges + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Badges have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding badges to eligible users: ", error); + throw error; + } +} + + +// actualizar le achieved del usuario +export async function updateUserAchievementBadge(uid, badgeId) { + try { + + const userRef = doc(firestoreDB, "users", uid); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.error("Usuario no encontrado"); + return; + } + + const userData = userDoc.data(); + const badges = userData.badges || []; + + const updatedBadges = badges.map((badge) => { + if (badge.id === badgeId) { + return { ...badge, achieved: true }; + } + return badge; + }); + + await updateDoc(userRef, { + badges: updatedBadges + }); + await invalidateUserCaches(uid, { includeLeaderboard: true }); + + // console.log(`El logro con id ${badgeId} se ha actualizado correctamente para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el logro del usuario:", error); + throw error; + } +} + + +// Contador de badges +export async function countAchievedBadges(uid) { + try { + const user = await getUser(uid); + + if (!user) { + console.error("Usuario no encontrado"); + return 0; + } + + const badges = user.badges || []; + return badges.reduce((count, badge) => count + (badge.achieved ? 1 : 0), 0); + } catch (error) { + console.error("Error al contar los logros alcanzados:", error); + throw error; + } +} + +// A??adir nuevos backgrounds a los usuarios sin borrar los existentes +export async function addBackgroundUsers() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const newBackgrounds = [ + { id: '8', image_url: '/assets/back/8.svg', bought: false, price: 100 }, + // Aquí puedes agregar más fondos nuevos... + ]; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + const currentBackgrounds = userData.background || []; + + // Crear un mapa de fondos existentes (para evitar duplicados) + const backgroundMap = new Map(currentBackgrounds.map(bg => [bg.id, bg])); + + // Añadir los nuevos fondos solo si no existen ya + newBackgrounds.forEach(bg => { + if (!backgroundMap.has(bg.id)) { + backgroundMap.set(bg.id, bg); + } + }); + + const mergedBackgrounds = Array.from(backgroundMap.values()); + + return updateDoc(userRef, { + background: mergedBackgrounds, + myBackground: userData.myBackground || "/assets/back/1.svg", // conservar el que ya tiene + useCoins: userData.useCoins || 0, // conservar las monedas que ya tiene + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Backgrounds have been merged successfully for eligible users."); + } catch (error) { + console.error("Error merging backgrounds for eligible users: ", error); + throw error; +} +} + + + +// actualizar le achieved del usuario +export async function updateUserBackground(uid, backId, coins, userCoins) { + try { + const userRef = doc(firestoreDB, "users", uid); + const userDoc = await getDoc(userRef); + + if (!userDoc.exists()) { + console.error("Usuario no encontrado"); + return; + } + + const userData = userDoc.data(); + const background = userData.background || []; + + const updatedBackground = background.map((back) => { + if (back.id === backId) { + return { ...back, bought: true }; + } + return back; + }); + + await updateDoc(userRef, { + background: updatedBackground, + useCoins: userCoins + coins + }); + await invalidateUserCaches(uid, { includeLeaderboard: true }); + + console.log(`El fondo con id ${backId} se ha actualizado correctamente para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el fondo del usuario:", error); + throw error; + } +} + +// Actualizar fondo +export async function updateUserBackgroundImage(uid, backgroundUrl) { + try { + const userRef = doc(firestoreDB, "users", uid); + await updateDoc(userRef, { + myBackground: backgroundUrl + }); + await invalidateUserCaches(uid); + console.log(`El fondo se ha actualizado a ${backgroundUrl} para el usuario ${uid}.`); + } catch (error) { + console.error("Error al actualizar el fondo del usuario:", error); + throw error; + } +} + + +export async function getTotalMaes(options = {}) { + try { + const maes = await getMaeDirectory(options); + return (maes ?? []).filter((user) => user.uid !== 'jackpot').length; + } catch (error) { + console.error("Error al obtener el total de MAEs: ", error); + throw error; + } +} + + + +// Añadir nuevas variables +export async function addExtraVariables() { + try { + const usersRef = collection(firestoreDB, "users"); + const querySnapshot = await getDocs(usersRef); + + const eligibleRoles = ['admin', 'coordi', 'mae', 'tec', 'publi']; + + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + const userData = doc.data(); + + if (eligibleRoles.includes(userData.role)) { + return updateDoc(userRef, { + asesoriasGrupales: 0, + }); + } else { + return Promise.resolve(); + } + }); + + await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + + console.log("Background have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding background to eligible users: ", error); + throw error; + } +} + + + +export async function clearUsersData() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "users")); + const rolesToUpdate = ["admin", "coordi", "mae", "tec", "publi"]; + + const updatePromises = []; + + querySnapshot.forEach(docSnapshot => { + const userData = docSnapshot.data(); + if (rolesToUpdate.includes(userData.role)) { + const userRef = doc(firestoreDB, "users", docSnapshot.id); + updatePromises.push(updateDoc(userRef, { + useCoins: userData.useCoins - userData.points, + subjects: [], + totalTime: 0, + points: 0 + })); + } + }); + + await Promise.all(updatePromises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log("Usuarios actualizados correctamente."); + } catch (error) { + console.error("Error al actualizar usuarios: ", error); + throw error; + } +} + + +export async function resetAllUsersTotalTimeAndPoints({ dryRun = false, batchSize = 450 } = {}) { + const usersSnap = await getDocs(collection(firestoreDB, "users")); + if (usersSnap.empty) return { scanned: 0, updated: 0 }; + + const docs = usersSnap.docs; + let updated = 0; + + if (dryRun) { + return { scanned: docs.length, updated: 0 }; + } + + for (let i = 0; i < docs.length; i += batchSize) { + const chunk = docs.slice(i, i + batchSize); + const batch = writeBatch(firestoreDB); + + chunk.forEach((d) => { + batch.update(d.ref, { totalTime: 0, points: 0 }); + }); + + await batch.commit(); + updated += chunk.length; + console.log(`✅ Restablecimiento en progreso: ${updated}/${docs.length}`); + } + + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); + console.log(`Reset complete. Se restablecieron totalTime y points para ${updated} usuarios.`); + return { scanned: docs.length, updated }; +} From 96a89f2a8bb42ed2e011e92f13df4efef27757e9 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:18:01 -0600 Subject: [PATCH 4/8] clean up .legacy files --- src/firebase/db/annoucement.js | 469 ++++++++++++++++++--- src/firebase/db/annoucement.legacy.js | 416 ------------------- src/firebase/db/asesorias.js | 566 +++++++++++++++++++------- src/firebase/db/asesorias.legacy.js | 455 --------------------- src/firebase/db/attendance.js | 251 ++++++++++-- src/firebase/db/attendance.legacy.js | 219 ---------- 6 files changed, 1053 insertions(+), 1323 deletions(-) delete mode 100644 src/firebase/db/annoucement.legacy.js delete mode 100644 src/firebase/db/asesorias.legacy.js delete mode 100644 src/firebase/db/attendance.legacy.js diff --git a/src/firebase/db/annoucement.js b/src/firebase/db/annoucement.js index 0c4e29289..38be7de8c 100644 --- a/src/firebase/db/annoucement.js +++ b/src/firebase/db/annoucement.js @@ -1,4 +1,19 @@ -import * as announcementDb from './annoucement.legacy'; +import { firestoreDB } from "../../main"; +import { + addDoc, + collection, + query, + getDocs, + where, + updateDoc, + doc, + getDoc, + deleteDoc +} from 'firebase/firestore'; +import { addAnnoucement } from "../img/users"; +import { + updatePoints +} from './users'; import { invalidateCacheTags, withCache } from '../cache/cache'; import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; @@ -7,11 +22,412 @@ async function invalidateAnnouncementCaches() { } export async function saveAnnouncement(announcementData, selectedFile) { - const result = await announcementDb.saveAnnouncement(announcementData, selectedFile); - await invalidateAnnouncementCaches(); + try { + console.log(announcementData.maesAsignados) + let imageUrl = ''; + + if (selectedFile) { + const filePath = `announcements/${announcementData.type}/${selectedFile.name}`; + imageUrl = await addAnnoucement(selectedFile, filePath); + } + const docRef = await addDoc(collection(firestoreDB, 'announcements'), { + ...announcementData, + imageUrl, + preregister: {}, + asistence: {}, + createdAt: new Date(), + visible: true + }); + + await invalidateAnnouncementCaches(); + return docRef.id; + } catch (error) { + console.error('Error al guardar el anuncio:', error); + throw error; + } +} + +async function fetchAnnouncementsEditFresh() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const querySnapshot = await getDocs(query(announcementsCollection)); + + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + })) + .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt )); + + + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +async function fetchAnnouncementsFresh() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + const q = query(announcementsCollection, where('visible', '==', true)); + const querySnapshot = await getDocs(q); + const now = new Date(); + console.log(querySnapshot.docs) + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + })) + .filter(announcement => { + // Filtrar por fecha válida + if (announcement.dateTime) { + const dateTime = announcement.dateTime.seconds + ? new Date(announcement.dateTime.seconds * 1000) + : new Date(announcement.dateTime); + if (dateTime < now && dateTime.toDateString() !== now.toDateString()) { + return false; + } + } + + // Filtrar por visible o tipo Especial + const isVisible = announcement.visible === true; + const isSpecial = announcement.id === undefined; + console.log(announcement.type) + console.log(announcement.visible) + return isVisible || isSpecial; + }) + .sort((a, b) => { + const dateA = a.createdAt.seconds ? new Date(a.createdAt.seconds * 1000) : new Date(a.createdAt); + const dateB = b.createdAt.seconds ? new Date(b.createdAt.seconds * 1000) : new Date(b.createdAt); + return dateA - dateB; + }); + console.log(announcements) + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +async function fetchAnnouncementsGrupalesFresh() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const q = query( + announcementsCollection, + where('type', '==', 'Asesoría'), + where('visible', '==', true) + ); + + const querySnapshot = await getDocs(q); + + const now = new Date(); + console.log(now); + const announcements = querySnapshot.docs + .map(doc => ({ + id: doc.id, + ...doc.data(), + dateTime: doc.data().dateTime.toDate(), + })) + .filter(ann => { + return ann.dateTime >= now || ann.dateTime.toDateString() === now.toDateString(); + }) + .sort((a, b) => a.createdAt.seconds - b.createdAt.seconds); // Ordenar por fecha de creación + + console.log(announcements); + return announcements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + + +export async function addUserToPreregsiter(announcementId, user) { + try { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + if (!announcementSnapshot.exists()) { + throw new Error(`El anuncio con ID ${announcementId} no existe.`); + } + + const announcementData = announcementSnapshot.data(); + + const currentPreregs = announcementData.preregister || {}; + if (currentPreregs[user.uid]) { + throw new Error('Usuario ya registrado'); + } + const updatedPreregs = { + ...currentPreregs, + [user.uid]: user, + }; + + const currentAsistence = announcementData.asistence || {}; + const updatedAsistence = { + ...currentAsistence, + [user.uid]: false, + }; + + await updateDoc(announcementRef, { + preregister: updatedPreregs, + asistence: updatedAsistence, + }); + + await invalidateAnnouncementCaches(); + console.log(`Usuario ${user.uid} agregado exitosamente a preregister y asistencia.`); + } catch (error) { + console.error('Error añadiendo usuario a preregister:', error); + throw error; + } +} + + +export async function processAsistence(announcementId) { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + const data = announcementSnapshot.data(); + + const preregister = data.preregister || {}; + const asistence = data.asistence || {}; + const dateTime = data.dateTime || ''; + + const preregisterKeys = Object.keys(preregister); + if (preregisterKeys.length === 0) { + console.log("No preregister data found."); + return []; + } + + const result = preregisterKeys.map(uid => { + const user = preregister[uid]; + + return { + uid: uid, + dateTime: dateTime, + name: user.name || '', + career: user.career || '', + area: user.area || '', + campus: user.campus || '', + asistence: asistence[uid] || false + }; + }); + + console.log("Result:", result); return result; } + +export async function updateUserAsistence(announcementId, userId) { + try { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + + const announcementSnapshot = await getDoc(announcementRef); + if (!announcementSnapshot.exists()) { + throw new Error(`El anuncio con ID ${announcementId} no existe.`); + } + + const announcementData = announcementSnapshot.data(); + const currentAsistence = announcementData.asistence || {}; + const maesAsignados = announcementData.maesAsignados || []; + + const newAsistenceStatus = !currentAsistence[userId]; + const updatedAsistence = { + ...currentAsistence, + [userId]: newAsistenceStatus, + }; + + const totalMaes = maesAsignados.length; + + if (totalMaes > 0) { + const pointsPerMae = 50 / totalMaes; + + + for (const mae of maesAsignados) { + if (newAsistenceStatus) { + + await updatePoints(mae.uid, pointsPerMae); + console.log(`Puntos distribuidos a ${mae.name}: +${pointsPerMae}`); + } else { + + await updatePoints(mae.uid, -pointsPerMae); + console.log(`Puntos distribuidos a ${mae.name}: -${pointsPerMae}`); + } + } + } else { + console.log('No hay MAEs asignados para asignar puntos.'); + } + + await updateDoc(announcementRef, { + asistence: updatedAsistence, + }); + + await invalidateAnnouncementCaches(); + console.log(`Asistencia para el usuario ${userId} actualizada exitosamente a ${newAsistenceStatus}.`); + } catch (error) { + console.error('Error actualizando la asistencia del usuario:', error); + throw error; + } +} + + +export async function processConfirms(announcementId) { + const announcementRef = doc(firestoreDB, 'announcements', announcementId); + const announcementSnapshot = await getDoc(announcementRef); + const data = announcementSnapshot.data(); + + const preregister = data.preregister || {}; + const asistence = data.asistence || {}; + const dateTime = data.dateTime || ''; + + const preregisterKeys = Object.keys(preregister); + if (preregisterKeys.length === 0) { + console.log("No preregister data found."); + return []; + } + + const result = preregisterKeys + .filter(uid => asistence[uid] === true) + .map(uid => { + const user = preregister[uid]; + console.log("Processing user:", user); + + return { + uid: uid, + dateTime: dateTime, + name: user.name || '', + career: user.career || '', + area: user.area || '', + campus: user.campus || '', + asistence: true + }; + }); + return result; +} + + +export async function addExtraVariables() { + try { + const usersRef = collection(firestoreDB, 'announcements'); + const querySnapshot = await getDocs(usersRef); + const promises = querySnapshot.docs.map(async (doc) => { + const userRef = doc.ref; + return updateDoc(userRef, { + maesAsignados: [] + }); + + }); + + await Promise.all(promises); + await invalidateAnnouncementCaches(); + + console.log("Background have been successfully added to eligible users."); + } catch (error) { + console.error("Error adding background to eligible users: ", error); + throw error; + } +} + +async function fetchAnnouncementsAllGrupalesFresh() { + try { + const announcementsCollection = collection(firestoreDB, 'announcements'); + + const q = query( + announcementsCollection, + where('type', '==', 'Asesoría') + ); + + const querySnapshot = await getDocs(q); + + const now = new Date(); + const announcements = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data(), + dateTime: doc.data().dateTime.toDate(), + })); + + const futureAnnouncements = announcements + .filter(announcement => + announcement.dateTime > now || + announcement.dateTime.toDateString() === now.toDateString() + ) + .sort((a, b) => a.dateTime - b.dateTime); + + const pastAnnouncements = announcements + .filter(announcement => + announcement.dateTime < now && + announcement.dateTime.toDateString() !== now.toDateString() + ) + .sort((a, b) => a.dateTime - b.dateTime); + + + const sortedAnnouncements = [...futureAnnouncements, ...pastAnnouncements]; + + + return sortedAnnouncements; + } catch (error) { + console.error('Error fetching announcements:', error); + throw error; + } +} + +export async function deleteAnnouncementById(id) { + try { + const announcementDocRef = doc(firestoreDB, "announcements", id); + + await deleteDoc(announcementDocRef); + await invalidateAnnouncementCaches(); + + console.log(`Announcement with ID ${id} deleted successfully.`); + } catch (error) { + console.error(`Error deleting announcement with ID ${id}:`, error); + throw error; + } +} + +export async function updateAnnouncement(announcementId, updatedData) { + try { + const docRef = doc(firestoreDB, 'announcements', announcementId); + + await updateDoc(docRef, { + ...updatedData, + }); + + await invalidateAnnouncementCaches(); + return docRef.id; + } catch (error) { + console.error('Error al actualizar el anuncio:', error); + throw error; + } +} + +export const toggleVisibilityById = async (id) => { + try { + console.log(id) + const dialogDocRef = doc(firestoreDB, 'announcements', id); + + const docSnap = await getDoc(dialogDocRef); + + if (docSnap.exists()) { + const currentVisibility = docSnap.data().visible + + await updateDoc(dialogDocRef, { + visible: !currentVisibility + }); + + await invalidateAnnouncementCaches(); + console.log(`Visibilidad del diálogo con ID ${id} actualizada correctamente`); + } else { + console.log("El documento no existe"); + } + } catch (error) { + console.error(`Error al actualizar la visibilidad del diálogo con ID ${id}:`, error); + throw error; + } +}; + export async function getAnnouncementsEdit(options = {}) { return await withCache( cacheKeys.announcementsEdit(), @@ -21,7 +437,7 @@ export async function getAnnouncementsEdit(options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ANNOUNCEMENTS] }, - async () => await announcementDb.getAnnouncementsEdit() + fetchAnnouncementsEditFresh ); } @@ -34,7 +450,7 @@ export async function getAnnouncements(options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ANNOUNCEMENTS] }, - async () => await announcementDb.getAnnouncements() + fetchAnnouncementsFresh ); } @@ -47,31 +463,10 @@ export async function getAnnouncementsGrupales(options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] }, - async () => await announcementDb.getAnnouncementsGrupales() + fetchAnnouncementsGrupalesFresh ); } -export async function addUserToPreregsiter(announcementId, user) { - const result = await announcementDb.addUserToPreregsiter(announcementId, user); - await invalidateAnnouncementCaches(); - return result; -} - -export const processAsistence = announcementDb.processAsistence; -export const processConfirms = announcementDb.processConfirms; - -export async function updateUserAsistence(announcementId, userId) { - const result = await announcementDb.updateUserAsistence(announcementId, userId); - await invalidateAnnouncementCaches(); - return result; -} - -export async function addExtraVariables() { - const result = await announcementDb.addExtraVariables(); - await invalidateAnnouncementCaches(); - return result; -} - export async function getAnnouncementsAllGrupales(options = {}) { return await withCache( cacheKeys.announcementsAllGroup(), @@ -81,24 +476,6 @@ export async function getAnnouncementsAllGrupales(options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS] }, - async () => await announcementDb.getAnnouncementsAllGrupales() + fetchAnnouncementsAllGrupalesFresh ); } - -export async function deleteAnnouncementById(id) { - const result = await announcementDb.deleteAnnouncementById(id); - await invalidateAnnouncementCaches(); - return result; -} - -export async function updateAnnouncement(announcementId, updatedData) { - const result = await announcementDb.updateAnnouncement(announcementId, updatedData); - await invalidateAnnouncementCaches(); - return result; -} - -export async function toggleVisibilityById(id) { - const result = await announcementDb.toggleVisibilityById(id); - await invalidateAnnouncementCaches(); - return result; -} diff --git a/src/firebase/db/annoucement.legacy.js b/src/firebase/db/annoucement.legacy.js deleted file mode 100644 index 851913ca8..000000000 --- a/src/firebase/db/annoucement.legacy.js +++ /dev/null @@ -1,416 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - addDoc, - collection, - query, - getDocs, - where, - updateDoc, - doc, - getDoc, - deleteDoc -} from 'firebase/firestore'; -import { addAnnoucement } from "../img/users"; -import { - updatePoints -} from './users'; - -export async function saveAnnouncement(announcementData, selectedFile) { - try { - console.log(announcementData.maesAsignados) - let imageUrl = ''; - - if (selectedFile) { - const filePath = `announcements/${announcementData.type}/${selectedFile.name}`; - imageUrl = await addAnnoucement(selectedFile, filePath); - } - const docRef = await addDoc(collection(firestoreDB, 'announcements'), { - ...announcementData, - imageUrl, - preregister: {}, - asistence: {}, - createdAt: new Date(), - visible: true - }); - - return docRef.id; - } catch (error) { - console.error('Error al guardar el anuncio:', error); - throw error; - } -} - -export async function getAnnouncementsEdit() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const querySnapshot = await getDocs(query(announcementsCollection)); - - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - })) - .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt )); - - - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function getAnnouncements() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - const q = query(announcementsCollection, where('visible', '==', true)); - const querySnapshot = await getDocs(q); - const now = new Date(); - console.log(querySnapshot.docs) - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - })) - .filter(announcement => { - // Filtrar por fecha válida - if (announcement.dateTime) { - const dateTime = announcement.dateTime.seconds - ? new Date(announcement.dateTime.seconds * 1000) - : new Date(announcement.dateTime); - if (dateTime < now && dateTime.toDateString() !== now.toDateString()) { - return false; - } - } - - // Filtrar por visible o tipo Especial - const isVisible = announcement.visible === true; - const isSpecial = announcement.id === undefined; - console.log(announcement.type) - console.log(announcement.visible) - return isVisible || isSpecial; - }) - .sort((a, b) => { - const dateA = a.createdAt.seconds ? new Date(a.createdAt.seconds * 1000) : new Date(a.createdAt); - const dateB = b.createdAt.seconds ? new Date(b.createdAt.seconds * 1000) : new Date(b.createdAt); - return dateA - dateB; - }); - console.log(announcements) - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function getAnnouncementsGrupales() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const q = query( - announcementsCollection, - where('type', '==', 'Asesoría'), - where('visible', '==', true) - ); - - const querySnapshot = await getDocs(q); - - const now = new Date(); - console.log(now); - const announcements = querySnapshot.docs - .map(doc => ({ - id: doc.id, - ...doc.data(), - dateTime: doc.data().dateTime.toDate(), - })) - .filter(ann => { - return ann.dateTime >= now || ann.dateTime.toDateString() === now.toDateString(); - }) - .sort((a, b) => a.createdAt.seconds - b.createdAt.seconds); // Ordenar por fecha de creación - - console.log(announcements); - return announcements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - - -export async function addUserToPreregsiter(announcementId, user) { - try { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - if (!announcementSnapshot.exists()) { - throw new Error(`El anuncio con ID ${announcementId} no existe.`); - } - - const announcementData = announcementSnapshot.data(); - - const currentPreregs = announcementData.preregister || {}; - if (currentPreregs[user.uid]) { - throw new Error('Usuario ya registrado'); - } - const updatedPreregs = { - ...currentPreregs, - [user.uid]: user, - }; - - const currentAsistence = announcementData.asistence || {}; - const updatedAsistence = { - ...currentAsistence, - [user.uid]: false, - }; - - await updateDoc(announcementRef, { - preregister: updatedPreregs, - asistence: updatedAsistence, - }); - - console.log(`Usuario ${user.uid} agregado exitosamente a preregister y asistencia.`); - } catch (error) { - console.error('Error añadiendo usuario a preregister:', error); - throw error; - } -} - - -export async function processAsistence(announcementId) { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - const data = announcementSnapshot.data(); - - const preregister = data.preregister || {}; - const asistence = data.asistence || {}; - const dateTime = data.dateTime || ''; - - const preregisterKeys = Object.keys(preregister); - if (preregisterKeys.length === 0) { - console.log("No preregister data found."); - return []; - } - - const result = preregisterKeys.map(uid => { - const user = preregister[uid]; - - return { - uid: uid, - dateTime: dateTime, - name: user.name || '', - career: user.career || '', - area: user.area || '', - campus: user.campus || '', - asistence: asistence[uid] || false - }; - }); - - console.log("Result:", result); - return result; -} - - -export async function updateUserAsistence(announcementId, userId) { - try { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - - const announcementSnapshot = await getDoc(announcementRef); - if (!announcementSnapshot.exists()) { - throw new Error(`El anuncio con ID ${announcementId} no existe.`); - } - - const announcementData = announcementSnapshot.data(); - const currentAsistence = announcementData.asistence || {}; - const maesAsignados = announcementData.maesAsignados || []; - - const newAsistenceStatus = !currentAsistence[userId]; - const updatedAsistence = { - ...currentAsistence, - [userId]: newAsistenceStatus, - }; - - const totalMaes = maesAsignados.length; - - if (totalMaes > 0) { - const pointsPerMae = 50 / totalMaes; - - - for (const mae of maesAsignados) { - if (newAsistenceStatus) { - - await updatePoints(mae.uid, pointsPerMae); - console.log(`Puntos distribuidos a ${mae.name}: +${pointsPerMae}`); - } else { - - await updatePoints(mae.uid, -pointsPerMae); - console.log(`Puntos distribuidos a ${mae.name}: -${pointsPerMae}`); - } - } - } else { - console.log('No hay MAEs asignados para asignar puntos.'); - } - - await updateDoc(announcementRef, { - asistence: updatedAsistence, - }); - - console.log(`Asistencia para el usuario ${userId} actualizada exitosamente a ${newAsistenceStatus}.`); - } catch (error) { - console.error('Error actualizando la asistencia del usuario:', error); - throw error; - } -} - - -export async function processConfirms(announcementId) { - const announcementRef = doc(firestoreDB, 'announcements', announcementId); - const announcementSnapshot = await getDoc(announcementRef); - const data = announcementSnapshot.data(); - - const preregister = data.preregister || {}; - const asistence = data.asistence || {}; - const dateTime = data.dateTime || ''; - - const preregisterKeys = Object.keys(preregister); - if (preregisterKeys.length === 0) { - console.log("No preregister data found."); - return []; - } - - const result = preregisterKeys - .filter(uid => asistence[uid] === true) - .map(uid => { - const user = preregister[uid]; - console.log("Processing user:", user); - - return { - uid: uid, - dateTime: dateTime, - name: user.name || '', - career: user.career || '', - area: user.area || '', - campus: user.campus || '', - asistence: true - }; - }); - return result; -} - - -export async function addExtraVariables() { - try { - const usersRef = collection(firestoreDB, 'announcements'); - const querySnapshot = await getDocs(usersRef); - const promises = querySnapshot.docs.map(async (doc) => { - const userRef = doc.ref; - return updateDoc(userRef, { - maesAsignados: [] - }); - - }); - - await Promise.all(promises); - - console.log("Background have been successfully added to eligible users."); - } catch (error) { - console.error("Error adding background to eligible users: ", error); - throw error; - } -} - -export async function getAnnouncementsAllGrupales() { - try { - const announcementsCollection = collection(firestoreDB, 'announcements'); - - const q = query( - announcementsCollection, - where('type', '==', 'Asesoría') - ); - - const querySnapshot = await getDocs(q); - - const now = new Date(); - const announcements = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data(), - dateTime: doc.data().dateTime.toDate(), - })); - - const futureAnnouncements = announcements - .filter(announcement => - announcement.dateTime > now || - announcement.dateTime.toDateString() === now.toDateString() - ) - .sort((a, b) => a.dateTime - b.dateTime); - - const pastAnnouncements = announcements - .filter(announcement => - announcement.dateTime < now && - announcement.dateTime.toDateString() !== now.toDateString() - ) - .sort((a, b) => a.dateTime - b.dateTime); - - - const sortedAnnouncements = [...futureAnnouncements, ...pastAnnouncements]; - - - return sortedAnnouncements; - } catch (error) { - console.error('Error fetching announcements:', error); - throw error; - } -} - -export async function deleteAnnouncementById(id) { - try { - const announcementDocRef = doc(firestoreDB, "announcements", id); - - await deleteDoc(announcementDocRef); - - console.log(`Announcement with ID ${id} deleted successfully.`); - } catch (error) { - console.error(`Error deleting announcement with ID ${id}:`, error); - throw error; - } -} - -export async function updateAnnouncement(announcementId, updatedData) { - try { - const docRef = doc(firestoreDB, 'announcements', announcementId); - - await updateDoc(docRef, { - ...updatedData, - }); - - return docRef.id; - } catch (error) { - console.error('Error al actualizar el anuncio:', error); - throw error; - } -} - -export const toggleVisibilityById = async (id) => { - try { - console.log(id) - const dialogDocRef = doc(firestoreDB, 'announcements', id); - - const docSnap = await getDoc(dialogDocRef); - - if (docSnap.exists()) { - const currentVisibility = docSnap.data().visible - - await updateDoc(dialogDocRef, { - visible: !currentVisibility - }); - - console.log(`Visibilidad del diálogo con ID ${id} actualizada correctamente`); - } else { - console.log("El documento no existe"); - } - } catch (error) { - console.error(`Error al actualizar la visibilidad del diálogo con ID ${id}:`, error); - throw error; - } -}; diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 2d098dc6f..37a2dae80 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -1,203 +1,455 @@ -import * as asesoriaDb from './asesorias.legacy'; -import { invalidateCacheTags, withCache } from '../cache/cache'; -import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; - -const SEMESTER_START = new Date('2024-08-05'); +import { firestoreDB } from "../../main"; +import { + addDoc, + collection, + query, + where, + getDocs, + Timestamp, + updateDoc, + doc, + deleteDoc, +} from 'firebase/firestore'; +import { + updatePoints, + updateUserAchievementBadge +} from './users'; + +// Registra la asesoría del mae +export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { + // Changing to use payload instead to debug it + const payload = { + peerInfo: { + uid: maeInfo.uid, + name: maeInfo.name, + career: maeInfo.career, + profilePictureUrl: maeInfo.photoURL || '', // Uses the photoURL pretty sure lol instead of profilePictureURL for some reason -_- + // Datos para el excel + area: maeInfo.area || '', + campus: maeInfo.campus || '' + }, + userInfo: { + uid: userInfo.uid, + name: userInfo.name, + career: userInfo.career, + profilePictureUrl: userInfo.photoURL || userInfo.profilePictureUrl || '', // Shouldn't matter because it's the student pero ps si se echan redesign at some point + // Datos para excel + area: userInfo.area || '', + campus: userInfo.campus || '', + // Added pq me interesa, could help in the future si queremos detectar cuanta de la gente son alumnos o si son maes entre ellos + role: userInfo.role + }, + rating, + comment, + subject, + date: Timestamp.now() + }; -function normalizeDateKey(date) { - if (!date) { - return 'all'; - } + // Debug para ver q se anden guardando los datos correctos + //console.log("Saving asesoria:", payload); - if (date instanceof Date) { - return date.toISOString(); - } + await addDoc(collection(firestoreDB, "asesorias"), payload); - return String(date); + updateExperienceAsesorias(maeInfo.uid, userInfo.uid, subject.id, Timestamp.now()); + return; } -function getCurrentSemesterRange() { +export async function getAsesoriasCountForUserInCurrentSemester(userId) { + try { const now = new Date(); const currentYear = now.getFullYear(); - - if (now.getMonth() < 6) { - return { - start: new Date(currentYear, 0, 1), - end: new Date(currentYear, 5, 30, 23, 59, 59, 999) - }; + let startOfSemester, endOfSemester; + + // Determine the current semester range + if (now.getMonth() < 6) { // January to June + startOfSemester = new Date(currentYear, 0, 1); // January 1st + endOfSemester = new Date(currentYear, 5, 30, 23, 59, 59, 999); // June 30th, end of day + } else { // July to December + startOfSemester = new Date(currentYear, 6, 1); // July 1st + endOfSemester = new Date(currentYear, 11, 31, 23, 59, 59, 999); // December 31st, end of day } - return { - start: new Date(currentYear, 6, 1), - end: new Date(currentYear, 11, 31, 23, 59, 59, 999) - }; + const startMs = startOfSemester.getTime(); + const endMs = endOfSemester.getTime(); + const requestsRef = collection(firestoreDB, "asesorias"); + + // Realiza la consulta solo por userId + const q = query(requestsRef, where("peerInfo.uid", "==", userId)); + const querySnapshot = await getDocs(q); + + const filteredCount = querySnapshot.docs.filter(doc => { + const data = doc.data(); + + // Normaliza date -> milisegundos + let ms; + const dt = data.date; + if (!dt) return false; + if (dt instanceof Timestamp) ms = dt.toMillis(); + else if (dt.toDate) ms = dt.toDate().getTime(); + else if (dt instanceof Date) ms = dt.getTime(); + else if (typeof dt === "string") ms = new Date(dt).getTime(); + else return false; + + const isDuplicate = data.duplicate === true; + + return ms >= startMs && ms <= endMs && !isDuplicate; + }).length; + + return filteredCount; + } catch (error) { + console.error("Error fetching request count: ", error); + return 0; + } } -async function invalidateAsesoriaCaches() { - await invalidateCacheTags([CACHE_TAGS.ASESORIAS]); -} -function timestampToMs(value) { - if (!value) return null; - if (typeof value?.toMillis === 'function') return value.toMillis(); - if (typeof value?.toDate === 'function') return value.toDate().getTime(); - if (value instanceof Date) return value.getTime(); - if (typeof value === 'string') return new Date(value).getTime(); - return null; -} +export async function getAsesorias(startDate = null, endDate = null) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); + let q; -export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { - const result = await asesoriaDb.addAsesoria(maeInfo, userInfo, subject, comment, rating); - await invalidateAsesoriaCaches(); - return result; -} + if (startDate && endDate) { + // Ajustar endDate para incluir todo el último día + const endDateAdjusted = new Date(endDate); + endDateAdjusted.setHours(23, 59, 59, 999); -export async function getAsesorias(startDate = null, endDate = null, options = {}) { - const startKey = normalizeDateKey(startDate); - const endKey = normalizeDateKey(endDate); - - return await withCache( - cacheKeys.asesoriasRange(startKey, endKey), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => await asesoriaDb.getAsesorias(startDate, endDate) - ); -} + const startTimestamp = Timestamp.fromDate(new Date(startDate)); + const endTimestamp = Timestamp.fromDate(endDateAdjusted); -export async function getAsesoriasCountForUserInCurrentSemester(userId, options = {}) { - const { start, end } = getCurrentSemesterRange(); - const semesterKey = `${start.getFullYear()}-${start.getMonth() < 6 ? '01' : '02'}`; - - return await withCache( - cacheKeys.asesoriasSemesterByPeer(userId, semesterKey), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => { - const asesorias = await getAsesorias(start, end, options); - return (asesorias ?? []).filter((doc) => { - const dateMs = timestampToMs(doc.date); - return ( - doc.peerInfo?.uid === userId && - doc.duplicate !== true && - dateMs !== null && - dateMs >= start.getTime() && - dateMs <= end.getTime() - ); - }).length; + q = query( + asesoriasRef, + where("date", ">=", startTimestamp), + where("date", "<=", endTimestamp) + ); + } else { + q = query(asesoriasRef); } - ); + + const querySnapshot = await getDocs(q); + const asesorias = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + + // Ordena las asesorías por fecha de la más reciente a la más antigua + asesorias.sort((a, b) => { + const dateA = a.date?.seconds || 0; + const dateB = b.date?.seconds || 0; + return dateB - dateA; + }); + + return asesorias; + } catch (error) { + console.error("Error fetching asesorias: ", error); + return []; + } } -export async function getAsesoriasByUid(uid, options = {}) { - const today = new Date(); - const asesorias = await getAsesorias(SEMESTER_START, today, options); - return (asesorias ?? []).filter((asesoria) => asesoria.peerInfo?.uid === uid); +// Función para obtener asesorías por UID, reutilizando getAsesorias +export async function getAsesoriasByUid(uid) { + try { + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + + const asesoriasFiltradas = asesorias.filter(asesoria => asesoria.peerInfo?.uid === uid); + + return asesoriasFiltradas; + } catch (error) { + console.error("Error fetching asesorias by UID: ", error); + return []; + } } export async function updateAllExperienceAsesorias() { - const result = await asesoriaDb.updateAllExperienceAsesorias(); - await invalidateAsesoriaCaches(); - return result; + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + const processed = new Set(); // Conjunto para evitar procesar la misma asesoría más de una vez + + for (const advisory of asesorias) { + const { peerInfo, userInfo, date, subject } = advisory; + const advisoryDate = date.toDate(); + + // Generar clave única para evitar reprocesar la misma asesoría + const key = `${peerInfo.uid}-${userInfo.uid}-${subject.id}-${advisoryDate.getTime()}`; + if (processed.has(key)) continue; // Si ya se procesó, omitimos esta asesoría + processed.add(key); // Marcar como procesada + + // Encontrar asesorías similares en un rango de 2 horas + const similarAdvisories = asesorias.filter(ad => { + const adDate = ad.date.toDate(); + return ( + ad.peerInfo.uid === peerInfo.uid && + ad.userInfo.uid === userInfo.uid && + ad.subject.id === subject.id && + Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000 // 2 horas en ms + ); + }); + + // Iteramos sobre las asesorías similares y actualizamos el campo 'duplicate' + for (let i = 0; i < similarAdvisories.length; i++) { + const ad = similarAdvisories[i]; + const isDuplicate = i > 0; // La primera no es duplicada, las demás sí + + try { + await updateAdvisoryDuplicateField(ad.id, isDuplicate); + console.log( + ad, + isDuplicate + ? "Marcada como duplicada" + : "Primera ocurrencia - No duplicada" + ); + } catch (error) { + console.error(`Error al actualizar la asesoría con ID: ${ad.id}`, error); + } + } + } } -export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { - const result = await asesoriaDb.updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate); - await invalidateAsesoriaCaches(); - return result; -} +// Función auxiliar para actualizar el campo 'duplicate' en una asesoría +async function updateAdvisoryDuplicateField(advisoryDate, isDuplicate) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); -export async function getCommentsByUid(uid, options = {}) { - const asesorias = await getAsesoriasByUid(uid, options); - return (asesorias ?? []).filter((asesoria) => asesoria.comment?.trim()); -} + const q = query(asesoriasRef, where("date", "==", advisoryDate)); + const querySnapshot = await getDocs(q); -export async function getAsesoriasByUidAndRating(uidUser, uidPeer = null, options = {}) { - return await withCache( - cacheKeys.asesoriasPendingRating(uidUser, uidPeer ?? 'all'), - { - ttlMs: CACHE_TTL_MS.ASESORIAS, - persist: true, - forceRefresh: options.forceRefresh ?? false, - tags: [CACHE_TAGS.ASESORIAS] - }, - async () => await asesoriaDb.getAsesoriasByUidAndRating(uidUser, uidPeer) - ); -} + if (querySnapshot.empty) { + console.log(`No se encontró ninguna asesoría con la fecha: ${advisoryDate}`); + return; + } -export async function updateAsesoria(id, data) { - const result = await asesoriaDb.updateAsesoria(id, data); - await invalidateAsesoriaCaches(); - return result; + const docRef = querySnapshot.docs[0].ref; + + await updateDoc(docRef, { duplicate: isDuplicate }); + + console.log(`Asesoría actualizada correctamente con fecha: ${advisoryDate}`); + } catch (error) { + console.error(`Error actualizando la asesoría con fecha ${advisoryDate}:`, error); + throw error; + } } -export async function getTotalAsesorias(startDate = null, endDate = null, options = {}) { - const asesorias = await getAsesorias(startDate, endDate, options); - return (asesorias ?? []).length; + +// Función para actualizar puntos basados en asesorías similares +export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { + try { + if (!(advisoryDate instanceof Date)) { + if (advisoryDate.toDate) { + advisoryDate = advisoryDate.toDate(); + } else { + advisoryDate = new Date(advisoryDate); + } + } + + const today = Timestamp.now().toDate(); + const startOfDay = new Date(today); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(today); + endOfDay.setHours(23, 59, 59, 999); + + const asesoriasRef = collection(firestoreDB, "asesorias"); + + const q = query( + asesoriasRef, + where("date", ">=", startOfDay), + where("date", "<=", endOfDay) + ); + + + const querySnapshot = await getDocs(q); + const asesorias = querySnapshot.docs.map(doc => doc.data()); + const similarAdvisories = asesorias.filter(ad => { + const adDate = ad.date.toDate(); + return ad.peerInfo.uid === peerUid && + ad.userInfo.uid === userUid && + ad.subject.id === subjectId && + Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000; + }); + const ultimoElemento = asesorias[asesorias.length - 1]; + if (similarAdvisories.length > 1) { + await updatePoints(peerUid, -150); + await updateAdvisoryDuplicateField(ultimoElemento.date, true ); + await updateUserAchievementBadge(userUid, "18"); + } else { + await updateAdvisoryDuplicateField(ultimoElemento.date, false ); + if(subjectId === "MAE"){ + await updatePoints(peerUid, 15); + }else{ + await updatePoints(peerUid, 60); + } + + } + + } catch (error) { + console.error("Error actualizando la experiencia de asesorías:", error); + } } -export async function getAsesoriasCountByUser(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const userAsesoriasSet = new Set((asesorias ?? []).map(doc => doc.userInfo?.uid).filter(Boolean)); - return userAsesoriasSet.size; +// Función para obtener asesorías por UID, reutilizando getAsesorias +export async function getCommentsByUid(uid) { + try { + const startDate = new Date('2024-08-05'); + const today = new Date(); + const asesorias = await getAsesorias(startDate, today); + + const asesoriasFiltradas = asesorias.filter(asesoria => + asesoria.peerInfo?.uid === uid && asesoria.comment?.trim() + ); + + return asesoriasFiltradas; + } catch (error) { + console.error("Error fetching asesorias by UID: ", error); + return []; + } } -export async function getAsesoriasCountByArea(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const areasCount = {}; - (asesorias ?? []).forEach((asesoria) => { - const subjectArea = asesoria?.subject?.area; - const userUid = asesoria?.userInfo?.uid; +export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); - if (!subjectArea || !userUid) { - return; - } + let queryConstraints = [ + where("userInfo.uid", "==", uidUser), + where("rating", "==", null), + where("duplicate", "==", false), + ]; - if (!areasCount[subjectArea]) { - areasCount[subjectArea] = { - totalAsesorias: 0, - userUids: new Set() - }; + if (uidPeer) { + queryConstraints.unshift( where("peerInfo.uid", "==", uidPeer)); } - areasCount[subjectArea].totalAsesorias++; - areasCount[subjectArea].userUids.add(userUid); - }); + const q = query(asesoriasRef, ...queryConstraints); + const querySnapshot = await getDocs(q); - return Object.keys(areasCount).map(area => ({ - area, - totalAsesorias: areasCount[area].totalAsesorias, - totalUniqueUsers: areasCount[area].userUids.size - })); + const asesorias = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data() + })); + + return asesorias; + } catch (error) { + console.error("Error fetching asesorias: ", error); + return []; + } +} +export async function updateAsesoria(id, data) { + try { + + const asesoriaRef = doc(firestoreDB, "asesorias", id); + await updateDoc(asesoriaRef, data); + + console.log("Asesoria actualizada exitosamente"); + } catch (error) { + console.error("Error updating asesoria: ", error); + } + } + + + export async function getTotalAsesorias(startDate = null, endDate = null) { + try { + const asesorias = await getAsesorias(startDate, endDate); + const totalAsesorias = asesorias.length; + return totalAsesorias; + } catch (error) { + console.error("Error fetching total asesorias: ", error); + return 0; + } } -export async function getAsesoriasCountByCampus(options = {}) { - const asesorias = await getAsesorias(null, null, options); - const campusCount = {}; - (asesorias ?? []).forEach((asesoria) => { - const campus = asesoria?.userInfo?.campus; - if (campus) { - campusCount[campus] = (campusCount[campus] || 0) + 1; - } - }); +export async function getAsesoriasCountByUser() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const userAsesoriasSet = new Set( + querySnapshot.docs.map(doc => doc.data().userInfo?.uid).filter(Boolean) + ); + return userAsesoriasSet.size; + } catch (error) { + console.error("Error al obtener el conteo de asesorías por usuario: ", error); + throw error; + } +} - return Object.keys(campusCount).map(campus => ({ - campus, - totalAsesorias: campusCount[campus] - })); +export async function getAsesoriasCountByArea() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const areasCount = {}; + + querySnapshot.docs.forEach(doc => { + const asesoríaData = doc.data(); + const subjectArea = asesoríaData?.subject?.area; + const userUid = asesoríaData?.userInfo?.uid; + + if (subjectArea && userUid) { + if (!areasCount[subjectArea]) { + areasCount[subjectArea] = { + totalAsesorias: 0, + userUids: new Set() + }; + } + + areasCount[subjectArea].totalAsesorias++; + areasCount[subjectArea].userUids.add(userUid); + } + }); + + return Object.keys(areasCount).map(area => ({ + area, + totalAsesorias: areasCount[area].totalAsesorias, + totalUniqueUsers: areasCount[area].userUids.size + })); + } catch (error) { + console.error("Error al obtener el conteo de asesorías y usuarios por área: ", error); + throw error; + } } + +export async function getAsesoriasCountByCampus() { + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const campusCount = {}; + + querySnapshot.forEach(doc => { + const campus = doc.data()?.userInfo?.campus; + if (campus) { + campusCount[campus] = (campusCount[campus] || 0) + 1; + } + }); + + return Object.keys(campusCount).map(campus => ({ + campus, + totalAsesorias: campusCount[campus] + })); + } catch (error) { + console.error("Error al obtener el conteo de asesorías por campus: ", error); + throw error; + } +} + +// Eliminar todas las asesorias pasadas export async function deleteOldAsesorias() { - const result = await asesoriaDb.deleteOldAsesorias(); - await invalidateAsesoriaCaches(); - return result; + try { + const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const currentYear = new Date().getFullYear(); + + const deletePromises = []; + + querySnapshot.forEach(docSnapshot => { + const asesoriasData = docSnapshot.data(); + const asesoriasDate = asesoriasData?.date ? new Date(asesoriasData.date) : null; + + if (asesoriasDate && asesoriasDate.getFullYear() !== currentYear) { + deletePromises.push(deleteDoc(doc(firestoreDB, "asesorias", docSnapshot.id))); + } + }); + + await Promise.all(deletePromises); + console.log("Asesorías antiguas eliminadas correctamente."); + } catch (error) { + console.error("Error al eliminar asesorías antiguas: ", error); + throw error; + } } diff --git a/src/firebase/db/asesorias.legacy.js b/src/firebase/db/asesorias.legacy.js deleted file mode 100644 index 076cf624f..000000000 --- a/src/firebase/db/asesorias.legacy.js +++ /dev/null @@ -1,455 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - addDoc, - collection, - query, - where, - getDocs, - Timestamp, - updateDoc, - doc, - deleteDoc, -} from 'firebase/firestore'; -import { - updatePoints, - updateUserAchievementBadge -} from './users'; - -// Registra la asesoría del mae -export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { - // Changing to use payload instead to debug it - const payload = { - peerInfo: { - uid: maeInfo.uid, - name: maeInfo.name, - career: maeInfo.career, - profilePictureUrl: maeInfo.photoURL || '', // Uses the photoURL pretty sure lol instead of profilePictureURL for some reason -_- - // Datos para el excel - area: maeInfo.area || '', - campus: maeInfo.campus || '' - }, - userInfo: { - uid: userInfo.uid, - name: userInfo.name, - career: userInfo.career, - profilePictureUrl: userInfo.photoURL || userInfo.profilePictureUrl || '', // Shouldn't matter because it's the student pero ps si se echan redesign at some point - // Datos para excel - area: userInfo.area || '', - campus: userInfo.campus || '', - // Added pq me interesa, could help in the future si queremos detectar cuanta de la gente son alumnos o si son maes entre ellos - role: userInfo.role - }, - rating, - comment, - subject, - date: Timestamp.now() - }; - - // Debug para ver q se anden guardando los datos correctos - //console.log("Saving asesoria:", payload); - - await addDoc(collection(firestoreDB, "asesorias"), payload); - - updateExperienceAsesorias(maeInfo.uid, userInfo.uid, subject.id, Timestamp.now()); - return; -} - -export async function getAsesoriasCountForUserInCurrentSemester(userId) { - try { - const now = new Date(); - const currentYear = now.getFullYear(); - let startOfSemester, endOfSemester; - - // Determine the current semester range - if (now.getMonth() < 6) { // January to June - startOfSemester = new Date(currentYear, 0, 1); // January 1st - endOfSemester = new Date(currentYear, 5, 30, 23, 59, 59, 999); // June 30th, end of day - } else { // July to December - startOfSemester = new Date(currentYear, 6, 1); // July 1st - endOfSemester = new Date(currentYear, 11, 31, 23, 59, 59, 999); // December 31st, end of day - } - - const startMs = startOfSemester.getTime(); - const endMs = endOfSemester.getTime(); - const requestsRef = collection(firestoreDB, "asesorias"); - - // Realiza la consulta solo por userId - const q = query(requestsRef, where("peerInfo.uid", "==", userId)); - const querySnapshot = await getDocs(q); - - const filteredCount = querySnapshot.docs.filter(doc => { - const data = doc.data(); - - // Normaliza date -> milisegundos - let ms; - const dt = data.date; - if (!dt) return false; - if (dt instanceof Timestamp) ms = dt.toMillis(); - else if (dt.toDate) ms = dt.toDate().getTime(); - else if (dt instanceof Date) ms = dt.getTime(); - else if (typeof dt === "string") ms = new Date(dt).getTime(); - else return false; - - const isDuplicate = data.duplicate === true; - - return ms >= startMs && ms <= endMs && !isDuplicate; - }).length; - - return filteredCount; - } catch (error) { - console.error("Error fetching request count: ", error); - return 0; - } -} - - -export async function getAsesorias(startDate = null, endDate = null) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - let q; - - if (startDate && endDate) { - // Ajustar endDate para incluir todo el último día - const endDateAdjusted = new Date(endDate); - endDateAdjusted.setHours(23, 59, 59, 999); - - const startTimestamp = Timestamp.fromDate(new Date(startDate)); - const endTimestamp = Timestamp.fromDate(endDateAdjusted); - - q = query( - asesoriasRef, - where("date", ">=", startTimestamp), - where("date", "<=", endTimestamp) - ); - } else { - q = query(asesoriasRef); - } - - const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - - // Ordena las asesorías por fecha de la más reciente a la más antigua - asesorias.sort((a, b) => { - const dateA = a.date?.seconds || 0; - const dateB = b.date?.seconds || 0; - return dateB - dateA; - }); - - return asesorias; - } catch (error) { - console.error("Error fetching asesorias: ", error); - return []; - } -} - -// Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getAsesoriasByUid(uid) { - try { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - - const asesoriasFiltradas = asesorias.filter(asesoria => asesoria.peerInfo?.uid === uid); - - return asesoriasFiltradas; - } catch (error) { - console.error("Error fetching asesorias by UID: ", error); - return []; - } -} - -export async function updateAllExperienceAsesorias() { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - const processed = new Set(); // Conjunto para evitar procesar la misma asesoría más de una vez - - for (const advisory of asesorias) { - const { peerInfo, userInfo, date, subject } = advisory; - const advisoryDate = date.toDate(); - - // Generar clave única para evitar reprocesar la misma asesoría - const key = `${peerInfo.uid}-${userInfo.uid}-${subject.id}-${advisoryDate.getTime()}`; - if (processed.has(key)) continue; // Si ya se procesó, omitimos esta asesoría - processed.add(key); // Marcar como procesada - - // Encontrar asesorías similares en un rango de 2 horas - const similarAdvisories = asesorias.filter(ad => { - const adDate = ad.date.toDate(); - return ( - ad.peerInfo.uid === peerInfo.uid && - ad.userInfo.uid === userInfo.uid && - ad.subject.id === subject.id && - Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000 // 2 horas en ms - ); - }); - - // Iteramos sobre las asesorías similares y actualizamos el campo 'duplicate' - for (let i = 0; i < similarAdvisories.length; i++) { - const ad = similarAdvisories[i]; - const isDuplicate = i > 0; // La primera no es duplicada, las demás sí - - try { - await updateAdvisoryDuplicateField(ad.id, isDuplicate); - console.log( - ad, - isDuplicate - ? "Marcada como duplicada" - : "Primera ocurrencia - No duplicada" - ); - } catch (error) { - console.error(`Error al actualizar la asesoría con ID: ${ad.id}`, error); - } - } - } -} - -// Función auxiliar para actualizar el campo 'duplicate' en una asesoría -async function updateAdvisoryDuplicateField(advisoryDate, isDuplicate) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - - const q = query(asesoriasRef, where("date", "==", advisoryDate)); - const querySnapshot = await getDocs(q); - - if (querySnapshot.empty) { - console.log(`No se encontró ninguna asesoría con la fecha: ${advisoryDate}`); - return; - } - - const docRef = querySnapshot.docs[0].ref; - - await updateDoc(docRef, { duplicate: isDuplicate }); - - console.log(`Asesoría actualizada correctamente con fecha: ${advisoryDate}`); - } catch (error) { - console.error(`Error actualizando la asesoría con fecha ${advisoryDate}:`, error); - throw error; - } -} - - -// Función para actualizar puntos basados en asesorías similares -export async function updateExperienceAsesorias(peerUid, userUid, subjectId, advisoryDate) { - try { - if (!(advisoryDate instanceof Date)) { - if (advisoryDate.toDate) { - advisoryDate = advisoryDate.toDate(); - } else { - advisoryDate = new Date(advisoryDate); - } - } - - const today = Timestamp.now().toDate(); - const startOfDay = new Date(today); - startOfDay.setHours(0, 0, 0, 0); - const endOfDay = new Date(today); - endOfDay.setHours(23, 59, 59, 999); - - const asesoriasRef = collection(firestoreDB, "asesorias"); - - const q = query( - asesoriasRef, - where("date", ">=", startOfDay), - where("date", "<=", endOfDay) - ); - - - const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => doc.data()); - const similarAdvisories = asesorias.filter(ad => { - const adDate = ad.date.toDate(); - return ad.peerInfo.uid === peerUid && - ad.userInfo.uid === userUid && - ad.subject.id === subjectId && - Math.abs(adDate.getTime() - advisoryDate.getTime()) <= 2 * 60 * 60 * 1000; - }); - const ultimoElemento = asesorias[asesorias.length - 1]; - if (similarAdvisories.length > 1) { - await updatePoints(peerUid, -150); - await updateAdvisoryDuplicateField(ultimoElemento.date, true ); - await updateUserAchievementBadge(userUid, "18"); - } else { - await updateAdvisoryDuplicateField(ultimoElemento.date, false ); - if(subjectId === "MAE"){ - await updatePoints(peerUid, 15); - }else{ - await updatePoints(peerUid, 60); - } - - } - - } catch (error) { - console.error("Error actualizando la experiencia de asesorías:", error); - } -} - -// Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getCommentsByUid(uid) { - try { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - - const asesoriasFiltradas = asesorias.filter(asesoria => - asesoria.peerInfo?.uid === uid && asesoria.comment?.trim() - ); - - return asesoriasFiltradas; - } catch (error) { - console.error("Error fetching asesorias by UID: ", error); - return []; - } -} - - -export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { - try { - const asesoriasRef = collection(firestoreDB, "asesorias"); - - let queryConstraints = [ - where("userInfo.uid", "==", uidUser), - where("rating", "==", null), - where("duplicate", "==", false), - ]; - - if (uidPeer) { - queryConstraints.unshift( where("peerInfo.uid", "==", uidPeer)); - } - - const q = query(asesoriasRef, ...queryConstraints); - const querySnapshot = await getDocs(q); - - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); - - return asesorias; - } catch (error) { - console.error("Error fetching asesorias: ", error); - return []; - } -} -export async function updateAsesoria(id, data) { - try { - - const asesoriaRef = doc(firestoreDB, "asesorias", id); - await updateDoc(asesoriaRef, data); - - console.log("Asesoria actualizada exitosamente"); - } catch (error) { - console.error("Error updating asesoria: ", error); - } - } - - - export async function getTotalAsesorias(startDate = null, endDate = null) { - try { - const asesorias = await getAsesorias(startDate, endDate); - const totalAsesorias = asesorias.length; - return totalAsesorias; - } catch (error) { - console.error("Error fetching total asesorias: ", error); - return 0; - } -} - - -export async function getAsesoriasCountByUser() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const userAsesoriasSet = new Set( - querySnapshot.docs.map(doc => doc.data().userInfo?.uid).filter(Boolean) - ); - return userAsesoriasSet.size; - } catch (error) { - console.error("Error al obtener el conteo de asesorías por usuario: ", error); - throw error; - } -} - -export async function getAsesoriasCountByArea() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const areasCount = {}; - - querySnapshot.docs.forEach(doc => { - const asesoríaData = doc.data(); - const subjectArea = asesoríaData?.subject?.area; - const userUid = asesoríaData?.userInfo?.uid; - - if (subjectArea && userUid) { - if (!areasCount[subjectArea]) { - areasCount[subjectArea] = { - totalAsesorias: 0, - userUids: new Set() - }; - } - - areasCount[subjectArea].totalAsesorias++; - areasCount[subjectArea].userUids.add(userUid); - } - }); - - return Object.keys(areasCount).map(area => ({ - area, - totalAsesorias: areasCount[area].totalAsesorias, - totalUniqueUsers: areasCount[area].userUids.size - })); - } catch (error) { - console.error("Error al obtener el conteo de asesorías y usuarios por área: ", error); - throw error; - } -} - - -export async function getAsesoriasCountByCampus() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const campusCount = {}; - - querySnapshot.forEach(doc => { - const campus = doc.data()?.userInfo?.campus; - if (campus) { - campusCount[campus] = (campusCount[campus] || 0) + 1; - } - }); - - return Object.keys(campusCount).map(campus => ({ - campus, - totalAsesorias: campusCount[campus] - })); - } catch (error) { - console.error("Error al obtener el conteo de asesorías por campus: ", error); - throw error; - } -} - -// Eliminar todas las asesorias pasadas -export async function deleteOldAsesorias() { - try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const currentYear = new Date().getFullYear(); - - const deletePromises = []; - - querySnapshot.forEach(docSnapshot => { - const asesoriasData = docSnapshot.data(); - const asesoriasDate = asesoriasData?.date ? new Date(asesoriasData.date) : null; - - if (asesoriasDate && asesoriasDate.getFullYear() !== currentYear) { - deletePromises.push(deleteDoc(doc(firestoreDB, "asesorias", docSnapshot.id))); - } - }); - - await Promise.all(deletePromises); - console.log("Asesorías antiguas eliminadas correctamente."); - } catch (error) { - console.error("Error al eliminar asesorías antiguas: ", error); - throw error; - } -} \ No newline at end of file diff --git a/src/firebase/db/attendance.js b/src/firebase/db/attendance.js index 7dc11e5b6..f613b17ca 100644 --- a/src/firebase/db/attendance.js +++ b/src/firebase/db/attendance.js @@ -1,12 +1,27 @@ -import * as attendanceDb from './attendance.legacy'; +import { firestoreDB } from "../../main"; +import { + doc, + getDoc, + getDocs, + setDoc, + collection, +} from 'firebase/firestore'; import { attendanceDateTag, CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; import { invalidateCacheTags, withCache } from '../cache/cache'; function getCurrentDateFormatted() { const today = new Date(); const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); + const month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based const day = String(today.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +function formatDateString(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } @@ -18,6 +33,206 @@ async function invalidateAttendanceForDate(dateString) { await invalidateCacheTags([CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)]); } +async function fetchTodaysReportFresh() { + try { + const reportRef = collection(firestoreDB, "attendance", getCurrentDateFormatted(), "report"); + // const reportRef = collection(firestoreDB, "attendance", "2024-05-16", "report"); + const reportSnapshot = await getDocs(reportRef); + + let report = {} + + reportSnapshot.forEach((doc) => { + const docData = doc.data(); + report[doc.id] = docData.report; + }); + + return report; + } catch (error) { + console.error("Error fetching filtered users: ", error); + return []; + } +} + +// Update the MAE attendance report w corresponding value +export async function updateReport(userInfo, report) { + try { + // Defensive checks + unwrap reactive proxy + const uid = userInfo?.uid ?? userInfo?.id ?? userInfo?.value?.uid; + const name = userInfo?.name ?? userInfo?.value?.name ?? ''; + const totalTime = userInfo?.totalTime ?? userInfo?.value?.totalTime ?? 0; + + console.log(uid, report, "Updating report") + const reportRef = doc(firestoreDB, "attendance", getCurrentDateFormatted(), "report", userInfo.uid); // Final de semestre, quitar report de aca y luego when accessing data para que sean menos datos + + // Stores less data for attendance + const dataUpload = { + id: userInfo.uid, // Student id + email: userInfo.email, // Student email, helps search data within firebase + name: userInfo.name, + totalTime: userInfo.totalTime, + report: report, // (A, R, F, J) + } + + console.log('Writing to Firestore path:', reportRef.path, 'payload:', dataUpload); + + const result = await setDoc(reportRef, dataUpload, { merge : true }); // Use merge so that it can keep otehr fields if write more data + await invalidateAttendanceForDate(getCurrentDateFormatted()); + return result; + } catch (error) { + console.error("Error updating the report: ", error); + return []; + } +} + +// Update attendance report for a specific date (used for makeup attendance) +export async function updateReportByDate(userInfo, date, report) { + try { + const dateString = formatDateString(date); + + const dateDocRef = doc(firestoreDB, "attendance", dateString); + await setDoc(dateDocRef, { initialized: true }, { merge: true }); + + const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); + await setDoc(reportRef, { + id: userInfo.uid, + email: userInfo.email, + name: userInfo.name, + totalTime: userInfo.totalTime, + report: report, + }, { merge: true }); + await invalidateAttendanceForDate(dateString); + } catch (error) { + console.error("Error updating report by date: ", error); + } +} + +// To get date info +export async function addRegister(userInfo, date) { + try { + const dateString = formatDateString(date); + + // Root date doc is created w dummy field + const dateDocRef = doc(firestoreDB, "attendance", dateString); + await setDoc(dateDocRef, { initialized: true }, { merge: true }); + + const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); + await setDoc(reportRef, { + ...userInfo, + report: 'RR' + }); + await invalidateAttendanceForDate(dateString); + + } catch (error) { + console.error("Error updating the report: ", error); + } +} + +async function fetchStudentReportFresh(uid) { + const d = new Date(); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const date = `${y}-${m}-${day}`; + const reportRef = doc(firestoreDB, "attendance", date, "report", uid); + const snap = await getDoc(reportRef); + + if (snap.exists()) { + return snap.data().report; // 'A', 'J', 'R', 'F' + } else { + return null; + } +} + + +// Para obtener los datos de asistencia de una fecha +async function fetchReportByDateFresh(dateString) { + try { + // Reference with root de attendance, document es dateString del input parameter, y luego report subcollection + const reportRef = collection(firestoreDB, "attendance", dateString, "report"); + const reportSnapshot = await getDocs(reportRef); // Arreglo del reporte + + let report = {}; // Objeto vacío para llenarlo de datos + + // For each reportSnapshot in array, extracts the data using document id as key (so matricula) + reportSnapshot.forEach((doc) => { + const docData = doc.data(); + report[doc.id] = docData; // or docData.report if needed + }); + + return report; + // Error debug + } catch (error) { + console.error("Error fetching report: ", error); + return {}; + } +} + +// Helper funct, gets dates between specified start and end date +function getDateStringsBetween(startDate, endDate) { + // Handle start and end differently because of time zones, shift to make them back because default at GMT-0600 so 6 hrs ahead -_- + const start = new Date(startDate + 'T12:00:00'); // Set to noon instead + const end = new Date(endDate + 'T12:00:00'); + const dateList = []; + + const currDate = new Date(start); // Sets start as current + + // Fetch all days in between the range + while (currDate <= end) { + const year = currDate.getFullYear(); + const month = String(currDate.getMonth() + 1).padStart(2, '0'); // Gets month, adds 0 if just one digit + const day = String(currDate.getDate()).padStart(2, '0'); // Gets date and adds 0 if just one digit + + //console.log(` Current date: ${currDate}, End date: ${end}`); + //console.log(` Comparison result: ${currDate <= end}`); + + // Save and upgrade for next iteration + dateList.push(`${year}-${month}-${day}`); // Adds formatted date to list for firebase use + currDate.setDate(currDate.getDate() + 1); // Moves to check next date + + //console.log(` After increment: ${currDate}`); + + + } + return dateList; +} + +// Gets the attendance reports for every day +async function fetchReportByDateRangeFresh(startDate, endDate) { + const dateStrings = getDateStringsBetween(startDate, endDate); + const report = []; + + // Checks each document date w the reports + for (const date of dateStrings) { + const reportRef = collection(firestoreDB, "attendance", date, "report"); + try { + const reportSnap = await getDocs(reportRef); + // Makes sure not empty date w no attendance + if (!reportSnap.empty) { + //console.log(`Found ${reportSnap.size} reports for ${date}`); + reportSnap.forEach((doc) => { + /*report.push({ + id: doc.id, + ...doc.data(), + date, + });*/ + const data = doc.data(); + // Only keeps id and report, modify if want other fields (like name or email) + report.push({ + id: doc.id, // Student matricula + report: data.report, // (A, R, F, J) + }); + }); + } else { + console.log(`No reports ${date}`); + } + } catch (error) { + console.warn(`Skipping ${date}:`, error.message); + } + } + + return report; +} + export async function getTodaysReport(options = {}) { const today = getCurrentDateFormatted(); @@ -29,34 +244,10 @@ export async function getTodaysReport(options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] }, - async () => await attendanceDb.getTodaysReport() + fetchTodaysReportFresh ); } -export async function updateReport(userInfo, report) { - const result = await attendanceDb.updateReport(userInfo, report); - await invalidateAttendanceForDate(getCurrentDateFormatted()); - return result; -} - -export async function updateReportByDate(userInfo, date, report) { - const result = await attendanceDb.updateReportByDate(userInfo, date, report); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - await invalidateAttendanceForDate(`${year}-${month}-${day}`); - return result; -} - -export async function addRegister(userInfo, date) { - const result = await attendanceDb.addRegister(userInfo, date); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - await invalidateAttendanceForDate(`${year}-${month}-${day}`); - return result; -} - export async function getStudentReport(uid, options = {}) { const today = getCurrentDateFormatted(); @@ -68,7 +259,7 @@ export async function getStudentReport(uid, options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(today)] }, - async () => await attendanceDb.getStudentReport(uid) + async () => await fetchStudentReportFresh(uid) ); } @@ -81,7 +272,7 @@ export async function getReportByDate(dateString, options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ATTENDANCE, attendanceDateTag(dateString)] }, - async () => await attendanceDb.getReportByDate(dateString) + async () => await fetchReportByDateFresh(dateString) ); } @@ -94,6 +285,6 @@ export async function getReportByDateRange(startDate, endDate, options = {}) { forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.ATTENDANCE] }, - async () => await attendanceDb.getReportByDateRange(startDate, endDate) + async () => await fetchReportByDateRangeFresh(startDate, endDate) ); } diff --git a/src/firebase/db/attendance.legacy.js b/src/firebase/db/attendance.legacy.js deleted file mode 100644 index ab6b83aba..000000000 --- a/src/firebase/db/attendance.legacy.js +++ /dev/null @@ -1,219 +0,0 @@ -import { firestoreDB } from "../../main"; -import { - doc, - getDoc, - getDocs, - setDoc, - collection, -} from 'firebase/firestore'; - -function getCurrentDateFormatted() { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based - const day = String(today.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; -} - -export async function getTodaysReport() { - try { - const reportRef = collection(firestoreDB, "attendance", getCurrentDateFormatted(), "report"); - // const reportRef = collection(firestoreDB, "attendance", "2024-05-16", "report"); - const reportSnapshot = await getDocs(reportRef); - - let report = {} - - reportSnapshot.forEach((doc) => { - const docData = doc.data(); - report[doc.id] = docData.report; - }); - - return report; - } catch (error) { - console.error("Error fetching filtered users: ", error); - return []; - } -} - -// Update the MAE attendance report w corresponding value -export async function updateReport(userInfo, report) { - try { - // Defensive checks + unwrap reactive proxy - const uid = userInfo?.uid ?? userInfo?.id ?? userInfo?.value?.uid; - const name = userInfo?.name ?? userInfo?.value?.name ?? ''; - const totalTime = userInfo?.totalTime ?? userInfo?.value?.totalTime ?? 0; - - console.log(uid, report, "Updating report") - const reportRef = doc(firestoreDB, "attendance", getCurrentDateFormatted(), "report", userInfo.uid); // Final de semestre, quitar report de aca y luego when accessing data para que sean menos datos - - // Stores less data for attendance - const dataUpload = { - id: userInfo.uid, // Student id - email: userInfo.email, // Student email, helps search data within firebase - name: userInfo.name, - totalTime: userInfo.totalTime, - report: report, // (A, R, F, J) - } - - console.log('Writing to Firestore path:', reportRef.path, 'payload:', dataUpload); - - return await setDoc(reportRef, dataUpload, { merge : true }); // Use merge so that it can keep otehr fields if write more data - } catch (error) { - console.error("Error updating the report: ", error); - return []; - } -} - -// Update attendance report for a specific date (used for makeup attendance) -export async function updateReportByDate(userInfo, date, report) { - try { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const dateString = `${year}-${month}-${day}`; - - const dateDocRef = doc(firestoreDB, "attendance", dateString); - await setDoc(dateDocRef, { initialized: true }, { merge: true }); - - const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); - await setDoc(reportRef, { - id: userInfo.uid, - email: userInfo.email, - name: userInfo.name, - totalTime: userInfo.totalTime, - report: report, - }, { merge: true }); - } catch (error) { - console.error("Error updating report by date: ", error); - } -} - -// To get date info -export async function addRegister(userInfo, date) { - try { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const dateString = `${year}-${month}-${day}`; - - // Root date doc is created w dummy field - const dateDocRef = doc(firestoreDB, "attendance", dateString); - await setDoc(dateDocRef, { initialized: true }, { merge: true }); - - const reportRef = doc(firestoreDB, "attendance", dateString, "report", userInfo.uid); - await setDoc(reportRef, { - ...userInfo, - report: 'RR' - }); - - } catch (error) { - console.error("Error updating the report: ", error); - } -} - -export async function getStudentReport(uid) { - const d = new Date(); - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const date = `${y}-${m}-${day}`; - const reportRef = doc(firestoreDB, "attendance", date, "report", uid); - const snap = await getDoc(reportRef); - - if (snap.exists()) { - return snap.data().report; // 'A', 'J', 'R', 'F' - } else { - return null; - } -} - - -// Para obtener los datos de asistencia de una fecha -export async function getReportByDate (dateString) { - try { - // Reference with root de attendance, document es dateString del input parameter, y luego report subcollection - const reportRef = collection(firestoreDB, "attendance", dateString, "report"); - const reportSnapshot = await getDocs(reportRef); // Arreglo del reporte - - let report = {}; // Objeto vacío para llenarlo de datos - - // For each reportSnapshot in array, extracts the data using document id as key (so matricula) - reportSnapshot.forEach((doc) => { - const docData = doc.data(); - report[doc.id] = docData; // or docData.report if needed - }); - - return report; - // Error debug - } catch (error) { - console.error("Error fetching report: ", error); - return {}; - } -} - -// Helper funct, gets dates between specified start and end date -function getDateStringsBetween(startDate, endDate) { - // Handle start and end differently because of time zones, shift to make them back because default at GMT-0600 so 6 hrs ahead -_- - const start = new Date(startDate + 'T12:00:00'); // Set to noon instead - const end = new Date(endDate + 'T12:00:00'); - const dateList = []; - - const currDate = new Date(start); // Sets start as current - - // Fetch all days in between the range - while (currDate <= end) { - const year = currDate.getFullYear(); - const month = String(currDate.getMonth() + 1).padStart(2, '0'); // Gets month, adds 0 if just one digit - const day = String(currDate.getDate()).padStart(2, '0'); // Gets date and adds 0 if just one digit - - //console.log(` Current date: ${currDate}, End date: ${end}`); - //console.log(` Comparison result: ${currDate <= end}`); - - // Save and upgrade for next iteration - dateList.push(`${year}-${month}-${day}`); // Adds formatted date to list for firebase use - currDate.setDate(currDate.getDate() + 1); // Moves to check next date - - //console.log(` After increment: ${currDate}`); - - - } - return dateList; -} - -// Gets the attendance reports for every day -export async function getReportByDateRange(startDate, endDate) { - const dateStrings = getDateStringsBetween(startDate, endDate); - const report = []; - - // Checks each document date w the reports - for (const date of dateStrings) { - const reportRef = collection(firestoreDB, "attendance", date, "report"); - try { - const reportSnap = await getDocs(reportRef); - // Makes sure not empty date w no attendance - if (!reportSnap.empty) { - //console.log(`Found ${reportSnap.size} reports for ${date}`); - reportSnap.forEach((doc) => { - /*report.push({ - id: doc.id, - ...doc.data(), - date, - });*/ - const data = doc.data(); - // Only keeps id and report, modify if want other fields (like name or email) - report.push({ - id: doc.id, // Student matricula - report: data.report, // (A, R, F, J) - }); - }); - } else { - console.log(`No reports ${date}`); - } - } catch (error) { - console.warn(`Skipping ${date}:`, error.message); - } - } - - return report; -} \ No newline at end of file From 810f0eb57621a74c9d40e3ff4147d6d9b54974bf Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:24:07 -0600 Subject: [PATCH 5/8] fix asesorias cache --- src/firebase/db/asesorias.js | 144 ++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 61 deletions(-) diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 37a2dae80..381b5ea1e 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -14,6 +14,52 @@ import { updatePoints, updateUserAchievementBadge } from './users'; +import { invalidateCacheTags, withCache } from '../cache/cache'; +import { CACHE_TAGS, CACHE_TTL_MS, cacheKeys } from '../cache/config'; + +const SEMESTER_START = new Date('2024-08-05'); + +function normalizeDateKey(date) { + if (!date) { + return 'all'; + } + + if (date instanceof Date) { + return date.toISOString(); + } + + return String(date); +} + +function getCurrentSemesterRange() { + const now = new Date(); + const currentYear = now.getFullYear(); + + if (now.getMonth() < 6) { + return { + start: new Date(currentYear, 0, 1), + end: new Date(currentYear, 5, 30, 23, 59, 59, 999) + }; + } + + return { + start: new Date(currentYear, 6, 1), + end: new Date(currentYear, 11, 31, 23, 59, 59, 999) + }; +} + +async function invalidateAsesoriaCaches() { + await invalidateCacheTags([CACHE_TAGS.ASESORIAS]); +} + +function timestampToMs(value) { + if (!value) return null; + if (typeof value?.toMillis === 'function') return value.toMillis(); + if (typeof value?.toDate === 'function') return value.toDate().getTime(); + if (value instanceof Date) return value.getTime(); + if (typeof value === 'string') return new Date(value).getTime(); + return null; +} // Registra la asesoría del mae export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { @@ -51,59 +97,40 @@ export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { await addDoc(collection(firestoreDB, "asesorias"), payload); updateExperienceAsesorias(maeInfo.uid, userInfo.uid, subject.id, Timestamp.now()); + await invalidateAsesoriaCaches(); return; } -export async function getAsesoriasCountForUserInCurrentSemester(userId) { - try { - const now = new Date(); - const currentYear = now.getFullYear(); - let startOfSemester, endOfSemester; - - // Determine the current semester range - if (now.getMonth() < 6) { // January to June - startOfSemester = new Date(currentYear, 0, 1); // January 1st - endOfSemester = new Date(currentYear, 5, 30, 23, 59, 59, 999); // June 30th, end of day - } else { // July to December - startOfSemester = new Date(currentYear, 6, 1); // July 1st - endOfSemester = new Date(currentYear, 11, 31, 23, 59, 59, 999); // December 31st, end of day - } - - const startMs = startOfSemester.getTime(); - const endMs = endOfSemester.getTime(); - const requestsRef = collection(firestoreDB, "asesorias"); - - // Realiza la consulta solo por userId - const q = query(requestsRef, where("peerInfo.uid", "==", userId)); - const querySnapshot = await getDocs(q); - - const filteredCount = querySnapshot.docs.filter(doc => { - const data = doc.data(); - - // Normaliza date -> milisegundos - let ms; - const dt = data.date; - if (!dt) return false; - if (dt instanceof Timestamp) ms = dt.toMillis(); - else if (dt.toDate) ms = dt.toDate().getTime(); - else if (dt instanceof Date) ms = dt.getTime(); - else if (typeof dt === "string") ms = new Date(dt).getTime(); - else return false; - - const isDuplicate = data.duplicate === true; - - return ms >= startMs && ms <= endMs && !isDuplicate; - }).length; - - return filteredCount; - } catch (error) { - console.error("Error fetching request count: ", error); - return 0; - } +export async function getAsesoriasCountForUserInCurrentSemester(userId, options = {}) { + const { start, end } = getCurrentSemesterRange(); + const semesterKey = `${start.getFullYear()}-${start.getMonth() < 6 ? '01' : '02'}`; + + return await withCache( + cacheKeys.asesoriasSemesterByPeer(userId, semesterKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => { + const asesorias = await getAsesorias(start, end, options); + return (asesorias ?? []).filter((doc) => { + const dateMs = timestampToMs(doc.date); + return ( + doc.peerInfo?.uid === userId && + doc.duplicate !== true && + dateMs !== null && + dateMs >= start.getTime() && + dateMs <= end.getTime() + ); + }).length; + } + ); } -export async function getAsesorias(startDate = null, endDate = null) { +async function fetchAsesoriasFresh(startDate = null, endDate = null) { try { const asesoriasRef = collection(firestoreDB, "asesorias"); let q; @@ -146,11 +173,10 @@ export async function getAsesorias(startDate = null, endDate = null) { } // Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getAsesoriasByUid(uid) { +export async function getAsesoriasByUid(uid, options = {}) { try { - const startDate = new Date('2024-08-05'); const today = new Date(); - const asesorias = await getAsesorias(startDate, today); + const asesorias = await getAsesorias(SEMESTER_START, today, options); const asesoriasFiltradas = asesorias.filter(asesoria => asesoria.peerInfo?.uid === uid); @@ -205,6 +231,7 @@ export async function updateAllExperienceAsesorias() { } } } + await invalidateAsesoriaCaches(); } // Función auxiliar para actualizar el campo 'duplicate' en una asesoría @@ -281,6 +308,7 @@ export async function updateExperienceAsesorias(peerUid, userUid, subjectId, adv } } + await invalidateAsesoriaCaches(); } catch (error) { console.error("Error actualizando la experiencia de asesorías:", error); @@ -288,17 +316,10 @@ export async function updateExperienceAsesorias(peerUid, userUid, subjectId, adv } // Función para obtener asesorías por UID, reutilizando getAsesorias -export async function getCommentsByUid(uid) { +export async function getCommentsByUid(uid, options = {}) { try { - const startDate = new Date('2024-08-05'); - const today = new Date(); - const asesorias = await getAsesorias(startDate, today); - - const asesoriasFiltradas = asesorias.filter(asesoria => - asesoria.peerInfo?.uid === uid && asesoria.comment?.trim() - ); - - return asesoriasFiltradas; + const asesorias = await getAsesoriasByUid(uid, options); + return asesorias.filter(asesoria => asesoria.comment?.trim()); } catch (error) { console.error("Error fetching asesorias by UID: ", error); return []; @@ -306,7 +327,7 @@ export async function getCommentsByUid(uid) { } -export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { +async function fetchAsesoriasByUidAndRatingFresh(uidUser , uidPeer = null) { try { const asesoriasRef = collection(firestoreDB, "asesorias"); @@ -339,6 +360,7 @@ export async function updateAsesoria(id, data) { const asesoriaRef = doc(firestoreDB, "asesorias", id); await updateDoc(asesoriaRef, data); + await invalidateAsesoriaCaches(); console.log("Asesoria actualizada exitosamente"); } catch (error) { From b79c3c5e181a38d4fee7d5467cbd9f7346240b18 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:24:32 -0600 Subject: [PATCH 6/8] second path for asesorias --- src/firebase/db/asesorias.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 381b5ea1e..ee1c3bfeb 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -369,9 +369,9 @@ export async function updateAsesoria(id, data) { } - export async function getTotalAsesorias(startDate = null, endDate = null) { + export async function getTotalAsesorias(startDate = null, endDate = null, options = {}) { try { - const asesorias = await getAsesorias(startDate, endDate); + const asesorias = await getAsesorias(startDate, endDate, options); const totalAsesorias = asesorias.length; return totalAsesorias; } catch (error) { @@ -381,12 +381,10 @@ export async function updateAsesoria(id, data) { } -export async function getAsesoriasCountByUser() { +export async function getAsesoriasCountByUser(options = {}) { try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); - const userAsesoriasSet = new Set( - querySnapshot.docs.map(doc => doc.data().userInfo?.uid).filter(Boolean) - ); + const asesorias = await getAsesorias(null, null, options); + const userAsesoriasSet = new Set((asesorias ?? []).map(doc => doc.userInfo?.uid).filter(Boolean)); return userAsesoriasSet.size; } catch (error) { console.error("Error al obtener el conteo de asesorías por usuario: ", error); From 9f910b5a04863eb4032580ef5b279186ee4f33c3 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 10:27:38 -0600 Subject: [PATCH 7/8] final patch --- src/firebase/db/asesorias.js | 49 ++++++++++++++++++++++++++++------- src/firebase/db/attendance.js | 2 -- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index ee1c3bfeb..a4f491a5d 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -392,15 +392,14 @@ export async function getAsesoriasCountByUser(options = {}) { } } -export async function getAsesoriasCountByArea() { +export async function getAsesoriasCountByArea(options = {}) { try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const asesorias = await getAsesorias(null, null, options); const areasCount = {}; - querySnapshot.docs.forEach(doc => { - const asesoríaData = doc.data(); - const subjectArea = asesoríaData?.subject?.area; - const userUid = asesoríaData?.userInfo?.uid; + (asesorias ?? []).forEach(asesoriaData => { + const subjectArea = asesoriaData?.subject?.area; + const userUid = asesoriaData?.userInfo?.uid; if (subjectArea && userUid) { if (!areasCount[subjectArea]) { @@ -427,13 +426,13 @@ export async function getAsesoriasCountByArea() { } -export async function getAsesoriasCountByCampus() { +export async function getAsesoriasCountByCampus(options = {}) { try { - const querySnapshot = await getDocs(collection(firestoreDB, "asesorias")); + const asesorias = await getAsesorias(null, null, options); const campusCount = {}; - querySnapshot.forEach(doc => { - const campus = doc.data()?.userInfo?.campus; + (asesorias ?? []).forEach(doc => { + const campus = doc?.userInfo?.campus; if (campus) { campusCount[campus] = (campusCount[campus] || 0) + 1; } @@ -467,9 +466,39 @@ export async function deleteOldAsesorias() { }); await Promise.all(deletePromises); + await invalidateAsesoriaCaches(); console.log("Asesorías antiguas eliminadas correctamente."); } catch (error) { console.error("Error al eliminar asesorías antiguas: ", error); throw error; } } + +export async function getAsesorias(startDate = null, endDate = null, options = {}) { + const startKey = normalizeDateKey(startDate); + const endKey = normalizeDateKey(endDate); + + return await withCache( + cacheKeys.asesoriasRange(startKey, endKey), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await fetchAsesoriasFresh(startDate, endDate) + ); +} + +export async function getAsesoriasByUidAndRating(uidUser, uidPeer = null, options = {}) { + return await withCache( + cacheKeys.asesoriasPendingRating(uidUser, uidPeer ?? 'all'), + { + ttlMs: CACHE_TTL_MS.ASESORIAS, + persist: true, + forceRefresh: options.forceRefresh ?? false, + tags: [CACHE_TAGS.ASESORIAS] + }, + async () => await fetchAsesoriasByUidAndRatingFresh(uidUser, uidPeer) + ); +} diff --git a/src/firebase/db/attendance.js b/src/firebase/db/attendance.js index f613b17ca..cc60f85f6 100644 --- a/src/firebase/db/attendance.js +++ b/src/firebase/db/attendance.js @@ -58,8 +58,6 @@ export async function updateReport(userInfo, report) { try { // Defensive checks + unwrap reactive proxy const uid = userInfo?.uid ?? userInfo?.id ?? userInfo?.value?.uid; - const name = userInfo?.name ?? userInfo?.value?.name ?? ''; - const totalTime = userInfo?.totalTime ?? userInfo?.value?.totalTime ?? 0; console.log(uid, report, "Updating report") const reportRef = doc(firestoreDB, "attendance", getCurrentDateFormatted(), "report", userInfo.uid); // Final de semestre, quitar report de aca y luego when accessing data para que sean menos datos From 703605fedc896cb058a5a5738db85a9ed890dea3 Mon Sep 17 00:00:00 2001 From: Jorge Adrian de la Garza Flores <52385984+jdelagarzaf@users.noreply.github.com> Date: Mon, 4 May 2026 12:18:07 -0600 Subject: [PATCH 8/8] less agressive cache policy for importatn data --- src/firebase/cache/config.js | 5 +++-- src/firebase/db/users.js | 4 ++-- src/views/AdminUsers.vue | 3 --- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/firebase/cache/config.js b/src/firebase/cache/config.js index 90c4f0349..68ce20e11 100644 --- a/src/firebase/cache/config.js +++ b/src/firebase/cache/config.js @@ -5,9 +5,10 @@ const DAY = 24 * HOUR; export const CACHE_TTL_MS = { CURRENT_USER: 5 * MINUTE, - USER: 15 * MINUTE, + USER: 5 * MINUTE, PROFILE_PICTURE: 30 * DAY, - MAE_DIRECTORY: 7 * DAY, + MAE_DIRECTORY: 10 * MINUTE, + MAES_TODAY: 1 * MINUTE, ACTIVE_MAES: 30 * SECOND, SUBJECTS: 7 * DAY, MAJORS: 7 * DAY, diff --git a/src/firebase/db/users.js b/src/firebase/db/users.js index 0322fd213..e9141393b 100644 --- a/src/firebase/db/users.js +++ b/src/firebase/db/users.js @@ -302,8 +302,8 @@ export async function getTodaysMae(options = {}) { return await withCache( cacheKeys.maesToday(getCurrentDayKey()), { - ttlMs: CACHE_TTL_MS.MAE_DIRECTORY, - persist: true, + ttlMs: CACHE_TTL_MS.MAES_TODAY, + persist: false, forceRefresh: options.forceRefresh ?? false, tags: [CACHE_TAGS.USERS, CACHE_TAGS.MAES] }, diff --git a/src/views/AdminUsers.vue b/src/views/AdminUsers.vue index 438bbbae1..53ed25458 100644 --- a/src/views/AdminUsers.vue +++ b/src/views/AdminUsers.vue @@ -1,12 +1,9 @@