diff --git a/src/firebase/cache/cache.js b/src/firebase/cache/cache.js new file mode 100644 index 000000000..523eeaf45 --- /dev/null +++ b/src/firebase/cache/cache.js @@ -0,0 +1,329 @@ +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) { + try { + await writePersistentEntry(entry); + } catch (error) { + console.warn(`Persistent cache write failed for ${key}; using memory cache only.`, error); + } + } + + 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) { + 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); + } + } + } + + 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); + try { + await deletePersistentEntry(key); + } catch (error) { + console.warn(`Persistent cache delete failed for ${key}.`, error); + } +} + +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); + } + } + + 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))); + } catch (error) { + console.warn('Persistent cache tag invalidation failed.', error); + } +} + +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..68ce20e11 --- /dev/null +++ b/src/firebase/cache/config.js @@ -0,0 +1,74 @@ +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: 5 * MINUTE, + PROFILE_PICTURE: 30 * DAY, + MAE_DIRECTORY: 10 * MINUTE, + MAES_TODAY: 1 * MINUTE, + 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', + announcementsAllGroup: () => 'announcements:group:all', + 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.js b/src/firebase/db/annoucement.js index 851913ca8..38be7de8c 100644 --- a/src/firebase/db/annoucement.js +++ b/src/firebase/db/annoucement.js @@ -14,6 +14,12 @@ 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'; + +async function invalidateAnnouncementCaches() { + await invalidateCacheTags([CACHE_TAGS.ANNOUNCEMENTS, CACHE_TAGS.GROUP_ANNOUNCEMENTS]); +} export async function saveAnnouncement(announcementData, selectedFile) { try { @@ -33,6 +39,7 @@ export async function saveAnnouncement(announcementData, selectedFile) { visible: true }); + await invalidateAnnouncementCaches(); return docRef.id; } catch (error) { console.error('Error al guardar el anuncio:', error); @@ -40,7 +47,7 @@ export async function saveAnnouncement(announcementData, selectedFile) { } } -export async function getAnnouncementsEdit() { +async function fetchAnnouncementsEditFresh() { try { const announcementsCollection = collection(firestoreDB, 'announcements'); @@ -62,7 +69,7 @@ export async function getAnnouncementsEdit() { } -export async function getAnnouncements() { +async function fetchAnnouncementsFresh() { try { const announcementsCollection = collection(firestoreDB, 'announcements'); const q = query(announcementsCollection, where('visible', '==', true)); @@ -106,7 +113,7 @@ export async function getAnnouncements() { } -export async function getAnnouncementsGrupales() { +async function fetchAnnouncementsGrupalesFresh() { try { const announcementsCollection = collection(firestoreDB, 'announcements'); @@ -170,6 +177,7 @@ export async function addUserToPreregsiter(announcementId, user) { 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); @@ -256,6 +264,7 @@ export async function updateUserAsistence(announcementId, userId) { 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); @@ -312,6 +321,7 @@ export async function addExtraVariables() { }); await Promise.all(promises); + await invalidateAnnouncementCaches(); console.log("Background have been successfully added to eligible users."); } catch (error) { @@ -320,7 +330,7 @@ export async function addExtraVariables() { } } -export async function getAnnouncementsAllGrupales() { +async function fetchAnnouncementsAllGrupalesFresh() { try { const announcementsCollection = collection(firestoreDB, 'announcements'); @@ -368,6 +378,7 @@ export async function deleteAnnouncementById(id) { const announcementDocRef = doc(firestoreDB, "announcements", id); await deleteDoc(announcementDocRef); + await invalidateAnnouncementCaches(); console.log(`Announcement with ID ${id} deleted successfully.`); } catch (error) { @@ -384,6 +395,7 @@ export async function updateAnnouncement(announcementId, updatedData) { ...updatedData, }); + await invalidateAnnouncementCaches(); return docRef.id; } catch (error) { console.error('Error al actualizar el anuncio:', error); @@ -405,6 +417,7 @@ export const toggleVisibilityById = async (id) => { visible: !currentVisibility }); + await invalidateAnnouncementCaches(); console.log(`Visibilidad del diálogo con ID ${id} actualizada correctamente`); } else { console.log("El documento no existe"); @@ -414,3 +427,55 @@ export const toggleVisibilityById = async (id) => { throw error; } }; + +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] + }, + fetchAnnouncementsEditFresh + ); +} + +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] + }, + fetchAnnouncementsFresh + ); +} + +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] + }, + fetchAnnouncementsGrupalesFresh + ); +} + +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] + }, + fetchAnnouncementsAllGrupalesFresh + ); +} diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 076cf624f..a4f491a5d 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) { @@ -347,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) { @@ -359,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); @@ -372,15 +392,14 @@ export async function getAsesoriasCountByUser() { } } -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]) { @@ -407,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; } @@ -447,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; } -} \ No newline at end of file +} + +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 ab6b83aba..cc60f85f6 100644 --- a/src/firebase/db/attendance.js +++ b/src/firebase/db/attendance.js @@ -6,6 +6,8 @@ import { 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(); @@ -16,7 +18,22 @@ function getCurrentDateFormatted() { return `${year}-${month}-${day}`; } -export async function getTodaysReport() { +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}`; +} + +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)]); +} + +async function fetchTodaysReportFresh() { try { const reportRef = collection(firestoreDB, "attendance", getCurrentDateFormatted(), "report"); // const reportRef = collection(firestoreDB, "attendance", "2024-05-16", "report"); @@ -41,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 @@ -58,7 +73,9 @@ export async function updateReport(userInfo, report) { 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 + 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 []; @@ -68,10 +85,7 @@ export async function updateReport(userInfo, report) { // 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 dateString = formatDateString(date); const dateDocRef = doc(firestoreDB, "attendance", dateString); await setDoc(dateDocRef, { initialized: true }, { merge: true }); @@ -84,6 +98,7 @@ export async function updateReportByDate(userInfo, date, report) { totalTime: userInfo.totalTime, report: report, }, { merge: true }); + await invalidateAttendanceForDate(dateString); } catch (error) { console.error("Error updating report by date: ", error); } @@ -92,10 +107,7 @@ export async function updateReportByDate(userInfo, date, report) { // 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}`; + const dateString = formatDateString(date); // Root date doc is created w dummy field const dateDocRef = doc(firestoreDB, "attendance", dateString); @@ -106,13 +118,14 @@ export async function addRegister(userInfo, date) { ...userInfo, report: 'RR' }); + await invalidateAttendanceForDate(dateString); } catch (error) { console.error("Error updating the report: ", error); } } -export async function getStudentReport(uid) { +async function fetchStudentReportFresh(uid) { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); @@ -130,7 +143,7 @@ export async function getStudentReport(uid) { // Para obtener los datos de asistencia de una fecha -export async function getReportByDate (dateString) { +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"); @@ -182,7 +195,7 @@ function getDateStringsBetween(startDate, endDate) { } // Gets the attendance reports for every day -export async function getReportByDateRange(startDate, endDate) { +async function fetchReportByDateRangeFresh(startDate, endDate) { const dateStrings = getDateStringsBetween(startDate, endDate); const report = []; @@ -216,4 +229,60 @@ export async function getReportByDateRange(startDate, endDate) { } return report; -} \ No newline at end of file +} + +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)] + }, + fetchTodaysReportFresh + ); +} + +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 fetchStudentReportFresh(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 fetchReportByDateFresh(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 fetchReportByDateRangeFresh(startDate, endDate) + ); +} diff --git a/src/firebase/db/campuses.js b/src/firebase/db/campuses.js index 7e32bbcf6..df9a2b948 100644 --- a/src/firebase/db/campuses.js +++ b/src/firebase/db/campuses.js @@ -3,15 +3,27 @@ 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() { - const campusesRef = collection(firestoreDB, "schools/tec.mx/campus"); +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); - const docsSnap = await getDocs(campusesRef); + if (docsSnap) { + return docsSnap.docs.map(doc => doc.data()); + } - if (docsSnap) { - return docsSnap.docs.map(doc => doc.data()); - } else { - return null; - } -} \ No newline at end of file + return null; + } + ); +} diff --git a/src/firebase/db/maeteca.js b/src/firebase/db/maeteca.js index 32bc62a56..709b38b21 100644 --- a/src/firebase/db/maeteca.js +++ b/src/firebase/db/maeteca.js @@ -14,16 +14,13 @@ export function filterVideosByText(videos, text) { import { firestoreDB } from "../../main"; import { getDocs, - getDoc, addDoc, - setDoc, - doc, collection, - query, - where, 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']; @@ -34,8 +31,24 @@ function assertVideoPermissions(user) { } } +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) { +export async function addVideoToMaeteca(videoData, { invalidate = true } = {}) { try { const user = await getCurrentUser(); if (!user) throw new Error('No authenticated user for write'); @@ -49,6 +62,9 @@ export async function addVideoToMaeteca(videoData) { }; const docRef = await addDoc(collection(firestoreDB, "videos"), payload); + if (invalidate) { + await invalidateVideoCaches(); + } console.log("Documento agregado con ID:", docRef.id); return docRef; } catch (error) { @@ -59,22 +75,18 @@ export async function addVideoToMaeteca(videoData) { // LEER todos los videos -export async function getAllVideos() { +export async function getAllVideos(options = {}) { 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; + 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; @@ -143,22 +155,28 @@ export function extractYoutubeId(url) { return null; } -export async function loadMaetecaVideos() { - const data = await getAllVideos(); +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) { +export async function getVideoById(id, options = {}) { 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() }; + 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; @@ -198,27 +216,21 @@ export function handleThumbnailKey(event, url) { } // BUSCAR por array "Relacionado" -export async function getVideosByRelated(relacionadoItem) { +export async function getVideosByRelated(relacionadoItem, options = {}) { try { - const videosRef = collection(firestoreDB, "videos"); - const q = query( - videosRef, - where("Relacionado", "array-contains", relacionadoItem) + 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)); + } ); - - 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; @@ -392,10 +404,11 @@ export async function createSampleVideos() { try { for (const item of samples) { // addVideoToMaeteca will attach createdBy and createdAt - const ref = await addVideoToMaeteca(item); + 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) { diff --git a/src/firebase/db/majors.js b/src/firebase/db/majors.js index a64c35f0b..9978e16db 100644 --- a/src/firebase/db/majors.js +++ b/src/firebase/db/majors.js @@ -3,15 +3,25 @@ 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() { - 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 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.js b/src/firebase/db/subjects.js index a77f62d19..55b1e5101 100644 --- a/src/firebase/db/subjects.js +++ b/src/firebase/db/subjects.js @@ -6,27 +6,40 @@ import { 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() { - const subjectsRef = collection(firestoreDB, "schools/tec.mx/subjects"); - - const q = query(subjectsRef, orderBy("name")); +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); - const docsSnap = await getDocs(q); + if (!docsSnap.empty) { + return docsSnap.docs.map(doc => doc.data()); + } - if (!docsSnap.empty) { - return docsSnap.docs.map(doc => doc.data()); - } else { - return null; - } + 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); - } \ No newline at end of file + await invalidateCacheTags([CACHE_TAGS.SUBJECTS]); + } diff --git a/src/firebase/db/users.js b/src/firebase/db/users.js index 47455ea80..e9141393b 100644 --- a/src/firebase/db/users.js +++ b/src/firebase/db/users.js @@ -17,8 +17,11 @@ import { 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) { @@ -29,6 +32,82 @@ function getEmailUsername(email) { 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); @@ -40,28 +119,49 @@ export async function createUser(userInfo) { userInfo.name = userInfo.firstname.trim() + ' ' + userInfo.lastname.trim(); const userRef = doc(firestoreDB, "users", userInfo.uid); - return await setDoc(userRef, userInfo); + const result = await setDoc(userRef, userInfo); + await invalidateUserCaches(userInfo.uid); + return result; } -export async function getUser(uid) { - const userRef = doc(firestoreDB, "users", uid); - const docSnap = await getDoc(userRef); +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 }; + } - if (docSnap.exists()) { - const data = docSnap.data() - const profilePictureUrl = await getUserProfilePicture(data.email); - return { ...data, profilePictureUrl }; - } else { - return null; - } + return null; + } + ); } -export async function getCurrentUser() { +export async function getCurrentUser(options = {}) { const auth = getAuth(); if (auth.currentUser) { const uid = getEmailUsername(auth.currentUser.email); - const user = await getUser(uid); - return user; + 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; } @@ -117,137 +217,47 @@ export const getClosestDayAndStartTime = (schedules) => { }; -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 getMaes(options = {}) { + return await getMaeDirectory(options); } -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 getMaesNames(options = {}) { + return await getMaeDirectory(options); } -export async function getUsersWithActiveSession(getProfilePicture = false) { +export async function getUsersWithActiveSession(getProfilePicture = false, options = {}) { 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; - }); + 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; + } - // 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 }; - }); + const fiveHoursAgoTimestampSeconds = Math.floor(Date.now() / 1000) - 18000; + const filteredDocs = querySnapshot.docs.filter((doc) => { + const data = doc.data(); + return data.activeSession?.startTime?.seconds > fiveHoursAgoTimestampSeconds; + }); - // Wait for all promises to resolve and return the users - return Promise.all(usersPromises); - } else { - return null; - } + 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); } @@ -256,14 +266,18 @@ export async function getUsersWithActiveSession(getProfilePicture = false) { 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); + const result = await updateDoc(userRef, userInfo); + await invalidateUserCaches(userId); + return result; } export async function updateUserSubjects(userId, newSubjects) { const userRef = doc(firestoreDB, "users", userId); - return await updateDoc(userRef, { + const result = await updateDoc(userRef, { subjects: newSubjects }); + await invalidateUserCaches(userId); + return result; } export async function updateUserSchedule(userId, newSchedule) { @@ -276,54 +290,38 @@ export async function updateUserSchedule(userId, newSchedule) { delete newSchedule[day]; } } - return await updateDoc(userRef, { + const result = await updateDoc(userRef, { weekSchedule: newSchedule }); + await invalidateUserCaches(userId); + return result; } -export async function getTodaysMae() { +export async function getTodaysMae(options = {}) { 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); + return await withCache( + cacheKeys.maesToday(getCurrentDayKey()), + { + ttlMs: CACHE_TTL_MS.MAES_TODAY, + persist: false, + 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; + }); } - }); - - // 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 []; @@ -333,7 +331,7 @@ export async function getTodaysMae() { export async function startActiveSession(userId, userInfo, location) { try { const userRef = doc(firestoreDB, "users", userId); - return await updateDoc(userRef, { + const result = await updateDoc(userRef, { activeSession: { peerInfo: userInfo, location, @@ -341,6 +339,8 @@ export async function startActiveSession(userId, userInfo, location) { startTime: serverTimestamp(), } }); + await invalidateUserCaches(userId, { includeActive: true }); + return result; } catch (error) { console.error("Error fetching filtered users: ", error); return []; @@ -372,6 +372,7 @@ export async function stopActiveSession(userId) { await updateDoc(userRef, { activeSession: deleteField() }); + await invalidateUserCaches(userId, { includeActive: true }); return { timeLimitExceded: true, activeSessionDeleted: false, differenceInMinutes } } @@ -382,6 +383,7 @@ export async function stopActiveSession(userId) { totalTime: totalTime, activeSession: deleteField() }); + await invalidateUserCaches(userId, { includeActive: true, includeLeaderboard: true }); return { totalTime, differenceInMinutes, activeSessionDeleted: true }; } catch (error) { @@ -395,6 +397,7 @@ export async function incrementTotalTime(userId, time) { await updateDoc(userRef, { totalTime: increment(time*60) }); + await invalidateUserCaches(userId, { includeLeaderboard: true }); } @@ -405,6 +408,7 @@ export async function updateUserProfilePicture(userId, photoURL) { await updateDoc(userRef, { photoURL: photoURL }); + await invalidateUserCaches(userId); } catch (error) { console.error('Error updating user profile picture: ', error); @@ -443,6 +447,7 @@ export async function clearAllUsersWeekSchedule() { }); 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) { @@ -501,6 +506,7 @@ export async function checkAndUpdateUserRole(file = null) { }); 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); @@ -529,6 +535,7 @@ export async function checkAndUpdateUserRole(file = null) { }); await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); console.log("Roles actualizados con base en weekSchedule, totalTime, y subjects."); } } catch (error) { @@ -614,6 +621,7 @@ export async function updateUserToMae(data) { }); 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); @@ -665,6 +673,7 @@ export const saveScheduleSubjectsExperience = async () => { }); await Promise.all(updatePromises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); } catch (error) { console.error("Error al guardar la experiencia:", error); } @@ -672,46 +681,40 @@ export const saveScheduleSubjectsExperience = async () => { 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 { + const userRef = doc(db, 'users', uid); + const userSnap = await getDoc(userRef); + + if (!userSnap.exists()) { console.log(`Usuario con uid ${uid} no encontrado.`); + return []; } - 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()); + const user = userSnap.data(); + const updatedPoints = (user.points || 0) + newPoints; - // Filtrar usuarios que tienen un nombre - data = data.filter(item => item.name); + await updateDoc(userRef, { points: updatedPoints }); + if (newPoints < 0) { + await updateUserAchievementBadge(uid, "18"); + } - // Ordenar por puntos de mayor a menor - data.sort((a, b) => b.points - a.points); + await invalidateUserCaches(uid, { includeLeaderboard: true }); + return [{ id: uid, ...user, points: updatedPoints }]; +} - return data; - } else { - return null; - } +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 @@ -757,6 +760,7 @@ export async function addBadgesToEligibleUsers() { }); await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); console.log("Badges have been successfully added to eligible users."); } catch (error) { @@ -791,6 +795,7 @@ export async function updateUserAchievementBadge(uid, badgeId) { 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) { @@ -803,28 +808,22 @@ export async function updateUserAchievementBadge(uid, badgeId) { // Contador de badges export async function countAchievedBadges(uid) { try { - const userRef = doc(firestoreDB, "users", uid); - const userDoc = await getDoc(userRef); + const user = await getUser(uid); - if (!userDoc.exists()) { + if (!user) { 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; + 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 +// A??adir nuevos backgrounds a los usuarios sin borrar los existentes export async function addBackgroundUsers() { try { const usersRef = collection(firestoreDB, "users"); @@ -867,6 +866,7 @@ export async function addBackgroundUsers() { }); await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); console.log("Backgrounds have been merged successfully for eligible users."); } catch (error) { @@ -902,6 +902,7 @@ export async function updateUserBackground(uid, backId, coins, userCoins) { 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) { @@ -917,6 +918,7 @@ export async function updateUserBackgroundImage(uid, backgroundUrl) { 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); @@ -925,17 +927,10 @@ export async function updateUserBackgroundImage(uid, backgroundUrl) { } -export async function getTotalMaes() { +export async function getTotalMaes(options = {}) { 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 ; + 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; @@ -966,6 +961,7 @@ export async function addExtraVariables() { }); await Promise.all(promises); + await invalidateUserCaches(null, { includeActive: true, includeLeaderboard: true }); console.log("Background have been successfully added to eligible users."); } catch (error) { @@ -997,6 +993,7 @@ export async function clearUsersData() { }); 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); @@ -1029,6 +1026,7 @@ export async function resetAllUsersTotalTimeAndPoints({ dryRun = false, batchSiz console.log(`✅ Restablecimiento en progreso: ${updated}/${docs.length}`); } - console.log(`🎉 Listo. Se restablecieron totalTime y points para ${updated} usuarios.`); + 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/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/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 @@