From 029e526d87ce4effbcf3aed7279a7834d0162919 Mon Sep 17 00:00:00 2001 From: Gael Quintanilla Date: Thu, 16 Apr 2026 12:37:01 -0600 Subject: [PATCH 1/5] Fixes de evaluaciones, leaderboard stats y cambiar carrera --- .gitignore | 1 + src/firebase/db/asesorias.js | 124 ++++++++++++++++++++++++++++------- src/firebase/db/settings.js | 32 +++++++++ src/firebase/db/users.js | 5 ++ src/layout/AppMenu.vue | 14 ++-- src/router/index.js | 15 ++--- src/views/AdminFunciones.vue | 63 ++++++++++++++++-- src/views/Evaluaciones.vue | 98 +++++++++++++++++---------- src/views/Inicio.vue | 42 +++++++----- src/views/Leaderboard.vue | 120 ++++++++++++++++++++++++++++++--- src/views/PerfilMAE.vue | 119 ++++++++++++++++++++++++++++----- 11 files changed, 513 insertions(+), 120 deletions(-) create mode 100644 src/firebase/db/settings.js diff --git a/.gitignore b/.gitignore index 541b719fb..292d817ab 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ public/themes/nano/ CLAUDE.md TODO.md BITACORA.md +PROPUESTA_LEADERBOARD.md diff --git a/src/firebase/db/asesorias.js b/src/firebase/db/asesorias.js index 076cf624f..8b178098d 100644 --- a/src/firebase/db/asesorias.js +++ b/src/firebase/db/asesorias.js @@ -42,6 +42,7 @@ export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { rating, comment, subject, + duplicate: false, date: Timestamp.now() }; @@ -54,6 +55,29 @@ export async function addAsesoria(maeInfo, userInfo, subject, comment, rating) { return; } +export async function backfillDuplicateField() { + try { + const asesoriasRef = collection(firestoreDB, "asesorias"); + const querySnapshot = await getDocs(asesoriasRef); + let updated = 0; + + const promises = querySnapshot.docs.map(async (docSnap) => { + const data = docSnap.data(); + if (data.duplicate === undefined) { + await updateDoc(doc(firestoreDB, "asesorias", docSnap.id), { duplicate: false }); + updated++; + } + }); + + await Promise.all(promises); + console.log(`Backfill completado: ${updated} asesorías actualizadas con duplicate: false`); + return updated; + } catch (error) { + console.error("Error en backfill de duplicate:", error); + return 0; + } +} + export async function getAsesoriasCountForUserInCurrentSemester(userId) { try { const now = new Date(); @@ -92,7 +116,7 @@ export async function getAsesoriasCountForUserInCurrentSemester(userId) { const isDuplicate = data.duplicate === true; - return ms >= startMs && ms <= endMs && !isDuplicate; + return ms >= startMs && ms <= endMs && !isDuplicate && !data._test; }).length; return filteredCount; @@ -126,10 +150,9 @@ export async function getAsesorias(startDate = null, endDate = null) { } const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); + const asesorias = querySnapshot.docs + .map(doc => ({ id: doc.id, ...doc.data() })) + .filter(a => !a._test && !a._type); // Ordena las asesorías por fecha de la más reciente a la más antigua asesorias.sort((a, b) => { @@ -287,33 +310,91 @@ 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 getEvaluacionesRecibidas(uid, revealedAt = undefined) { 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() + const asesoriasRef = collection(firestoreDB, "asesorias"); + const q = query( + asesoriasRef, + where("peerInfo.uid", "==", uid) ); - - return asesoriasFiltradas; + const querySnapshot = await getDocs(q); + return querySnapshot.docs + .map(doc => ({ id: doc.id, ...doc.data() })) + .filter(a => a.rating != null && a.duplicate !== true && !a._test) + .filter(a => { + if (revealedAt === undefined) return true; + if (!revealedAt) return false; + return (a.date?.seconds || 0) <= (revealedAt.seconds || 0); + }) + .sort((a, b) => (b.date?.seconds || 0) - (a.date?.seconds || 0)); } catch (error) { - console.error("Error fetching asesorias by UID: ", error); + console.error("Error fetching evaluaciones recibidas:", error); return []; } } +export async function getCommentsByUid(uid) { + return getEvaluacionesRecibidas(uid); +} + +export async function eliminarAsesoriasDePrueba() { + const asesoriasRef = collection(firestoreDB, "asesorias"); + const q = query(asesoriasRef, where("userInfo.uid", "==", "test-student-001")); + const snap = await getDocs(q); + let deleted = 0; + for (const docSnap of snap.docs) { + await deleteDoc(doc(firestoreDB, "asesorias", docSnap.id)); + deleted++; + } + const qConfig = query(asesoriasRef, where("_type", "==", "reveal_config")); + const snapConfig = await getDocs(qConfig); + for (const docSnap of snapConfig.docs) { + await deleteDoc(doc(firestoreDB, "asesorias", docSnap.id)); + deleted++; + } + const oldConfig = doc(firestoreDB, "asesorias", "_config"); + try { await deleteDoc(oldConfig); deleted++; } catch (e) { /* may not exist */ } + return deleted; +} + +export async function crearEvaluacionDePrueba(maeInfo, rating, comment) { + const fake = { + peerInfo: { + uid: maeInfo.uid, + name: maeInfo.name, + career: maeInfo.career || 'N/A', + profilePictureUrl: maeInfo.photoURL || maeInfo.profilePictureUrl || '', + area: maeInfo.area || '', + campus: maeInfo.campus || '' + }, + userInfo: { + uid: 'test-student-001', + name: 'Estudiante de Prueba', + career: 'ITC', + profilePictureUrl: '', + area: 'CIS', + campus: 'MTY', + role: 'user' + }, + subject: { id: 'TEST001', area: 'CIS', name: 'Materia de Prueba' }, + rating, + comment, + duplicate: false, + date: Timestamp.now(), + _test: true + }; + await addDoc(collection(firestoreDB, "asesorias"), fake); +} + + export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { try { const asesoriasRef = collection(firestoreDB, "asesorias"); let queryConstraints = [ - where("userInfo.uid", "==", uidUser), + where("userInfo.uid", "==", uidUser), where("rating", "==", null), - where("duplicate", "==", false), ]; if (uidPeer) { @@ -323,10 +404,9 @@ export async function getAsesoriasByUidAndRating(uidUser , uidPeer = null) { const q = query(asesoriasRef, ...queryConstraints); const querySnapshot = await getDocs(q); - const asesorias = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data() - })); + const asesorias = querySnapshot.docs + .map(doc => ({ id: doc.id, ...doc.data() })) + .filter(a => a.duplicate !== true); return asesorias; } catch (error) { diff --git a/src/firebase/db/settings.js b/src/firebase/db/settings.js new file mode 100644 index 000000000..012b3edaa --- /dev/null +++ b/src/firebase/db/settings.js @@ -0,0 +1,32 @@ +import { firestoreDB } from "../../main"; +import { collection, addDoc, query, where, getDocs, Timestamp, orderBy, limit } from 'firebase/firestore'; + +export async function getEvaluationsRevealedAt() { + try { + const q = query( + collection(firestoreDB, "asesorias"), + where("_type", "==", "reveal_config") + ); + const snap = await getDocs(q); + if (snap.empty) return null; + + let latest = null; + snap.docs.forEach(doc => { + const ts = doc.data().evaluationsRevealedAt; + if (ts && (!latest || ts.seconds > latest.seconds)) { + latest = ts; + } + }); + return latest; + } catch (error) { + console.error("Error fetching settings:", error); + return null; + } +} + +export async function revelarEvaluaciones() { + await addDoc(collection(firestoreDB, "asesorias"), { + _type: "reveal_config", + evaluationsRevealedAt: Timestamp.now() + }); +} diff --git a/src/firebase/db/users.js b/src/firebase/db/users.js index 47455ea80..4e98d8ead 100644 --- a/src/firebase/db/users.js +++ b/src/firebase/db/users.js @@ -259,6 +259,11 @@ export async function updateUserInfo(userId, userInfo) { return await updateDoc(userRef, userInfo); } +export async function updateUserCareer(userId, career, area) { + const userRef = doc(firestoreDB, "users", userId); + return await updateDoc(userRef, { career, area }); +} + export async function updateUserSubjects(userId, newSubjects) { const userRef = doc(firestoreDB, "users", userId); return await updateDoc(userRef, { diff --git a/src/layout/AppMenu.vue b/src/layout/AppMenu.vue index 1b66fcd15..140705b20 100644 --- a/src/layout/AppMenu.vue +++ b/src/layout/AppMenu.vue @@ -27,7 +27,6 @@ onMounted(async () => { if (['admin', 'tec'].includes(role)) { const adminItems = [ - { label: 'Usuarios', icon: 'pi pi-fw pi-users', to: '/admin/usuarios' }, { label: 'Materias', icon: 'pi pi-fw pi-pencil', to: '/admin/materias' } ]; @@ -36,7 +35,7 @@ onMounted(async () => { adminItems.push( { label: 'Asesorías', icon: 'pi pi-fw pi-list', to: '/admin/asesorias' }, { label: 'Funciones', icon: 'pi pi-fw pi-key', to: '/admin/funciones' }, - { label: 'Dashboard', icon: 'pi pi-fw pi-chart-bar', to: '/admin/dashboard' }, + { label: 'Dashboard', icon: 'pi pi-fw pi-chart-bar', to: '/admin/dashboard' }, { label: 'Historial asistencia', icon: 'pi pi-fw pi-history', to: '/admin/historialAsistencia'} ); } @@ -47,9 +46,7 @@ onMounted(async () => { }); } - - - if (['publi','mae', 'coordi', 'subjectCoordi', 'admin','tec'].includes(role)) { + if (['publi', 'mae', 'coordi', 'subjectCoordi', 'admin', 'tec'].includes(role)) { model.value.push({ label: 'MAE', items: [ @@ -59,18 +56,17 @@ onMounted(async () => { { label: 'Mis evaluaciones', icon:'pi pi-fw pi-heart', to: '/misevaluaciones'}, { label: 'Asistencia grupales', icon:'pi pi-fw pi-th-large', to: '/asistenciaGrupales'}, ] - }) + }); } - if (['coordi', 'subjectCoordi', 'admin','tec'].includes(role)) { + if (['coordi', 'subjectCoordi', 'admin', 'tec'].includes(role)) { model.value.push({ label: 'Coordi', items: [ { label: 'Asistencia', icon: 'pi pi-fw pi-check-square', to: '/coordi' }, { label: 'Gestión de anuncios', icon: 'pi pi-fw pi-cog', to: '/gestionAnuncios' }, - ] - }) + }); } // model.value.push({ diff --git a/src/router/index.js b/src/router/index.js index 185c296c2..7f24e8a6a 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -109,7 +109,7 @@ const router = createRouter({ name: 'coordi', component: () => import('@/views/Coordi.vue'), meta: { - roles: ['admin', 'coordi','tec'] + roles: ['admin', 'coordi', 'tec'] } }, { @@ -117,7 +117,7 @@ const router = createRouter({ name: 'gestionAnuncios', component: () => import('@/views/GestionAnuncios.vue'), meta: { - roles: ['admin', 'coordi','tec'] + roles: ['admin', 'coordi', 'tec'] } }, { @@ -125,7 +125,7 @@ const router = createRouter({ name: 'adminasesorias', component: () => import('@/views/AdminAsesorias.vue'), meta: { - roles: ['admin','tec'] + roles: ['admin', 'tec'] } }, { @@ -133,7 +133,7 @@ const router = createRouter({ name: 'adminusuarios', component: () => import('@/views/AdminUsers.vue'), meta: { - roles: ['admin' ,'tec'] + roles: ['admin', 'tec'] } }, { @@ -141,7 +141,7 @@ const router = createRouter({ name: 'adminmaterias', component: () => import('@/views/AdminSubjects.vue'), meta: { - roles: ['admin','tec'] + roles: ['admin', 'tec'] } }, { @@ -149,7 +149,7 @@ const router = createRouter({ name: 'adminfunciones', component: () => import('@/views/AdminFunciones.vue'), meta: { - roles: ['admin','tec'] + roles: ['admin', 'tec'] } }, { @@ -157,10 +157,9 @@ const router = createRouter({ name: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { - roles: ['admin','tec'] + roles: ['admin', 'tec'] } }, - /* Adding path for historial */ { path: '/admin/historialAsistencia', name: 'asistencia', diff --git a/src/views/AdminFunciones.vue b/src/views/AdminFunciones.vue index 480cf3572..f8d6c5e7c 100644 --- a/src/views/AdminFunciones.vue +++ b/src/views/AdminFunciones.vue @@ -15,7 +15,8 @@ import { clearUsersData, resetAllUsersTotalTimeAndPoints } from '../firebase/db/users'; -import { deleteOldAsesorias} from '../firebase/db/asesorias.js' +import { deleteOldAsesorias, eliminarAsesoriasDePrueba } from '../firebase/db/asesorias.js' +import { revelarEvaluaciones } from '../firebase/db/settings' const toast = useToast(); const confirm = useConfirm(); @@ -88,6 +89,49 @@ const restartMaes = () => { }); }; +const confirmLimpiarPruebas = () => { + confirm.require({ + message: '¿Eliminar todas las asesorías/evaluaciones de prueba de Firestore?', + header: 'Limpiar datos de prueba', + icon: 'pi pi-trash', + acceptLabel: 'Sí, eliminar', + rejectLabel: 'Cancelar', + acceptClass: 'p-button-danger', + accept: async () => { + try { + const count = await eliminarAsesoriasDePrueba(); + toast.add({ severity: 'success', summary: 'Éxito', detail: `Se eliminaron ${count} documentos de prueba.`, life: 3000 }); + } catch (error) { + console.error("Error al limpiar pruebas:", error); + toast.add({ severity: 'error', summary: 'Error', detail: 'Ocurrió un error: ' + error.message, life: 5000 }); + } + } + }); +}; + +const confirmRevealEvaluaciones = () => { + confirm.require({ + message: '¿Estás seguro de revelar todas las evaluaciones pendientes a los MAEs? Las evaluaciones creadas después de este momento permanecerán ocultas hasta la próxima revelación.', + header: 'Revelar evaluaciones', + icon: 'pi pi-eye', + acceptLabel: 'Sí, revelar', + rejectLabel: 'Cancelar', + acceptClass: 'p-button-success', + accept: async () => { + try { + await revelarEvaluaciones(); + toast.add({ severity: 'success', summary: 'Éxito', detail: 'Las evaluaciones han sido reveladas a los MAEs.', life: 3000 }); + } catch (error) { + console.error("Error al revelar evaluaciones:", error); + toast.add({ severity: 'error', summary: 'Error', detail: 'Ocurrió un error al revelar las evaluaciones.', life: 3000 }); + } + }, + reject: () => { + toast.add({ severity: 'info', summary: 'Cancelado', detail: 'No se han realizado cambios.', life: 3000 }); + } + }); +}; + const confirmDeleteAsesorias = () => { confirm.require({ message: '¿Estás seguro eliminar todas las asesorías deñ año pasado?', @@ -335,11 +379,20 @@ const handleUpdatePoints = async () => {
-
+ +
+
diff --git a/src/views/Evaluaciones.vue b/src/views/Evaluaciones.vue index 3bd7acad9..0857ee77b 100644 --- a/src/views/Evaluaciones.vue +++ b/src/views/Evaluaciones.vue @@ -1,12 +1,14 @@ - diff --git a/src/views/Inicio.vue b/src/views/Inicio.vue index 572dafa56..398844c9d 100644 --- a/src/views/Inicio.vue +++ b/src/views/Inicio.vue @@ -48,6 +48,7 @@ const anuncios = ref([]); const currentAnuncio = ref({}); const currentIndex = ref(-1); const isSavingAsesoria = ref(false); +const isSavingEval = ref(false); const evalInfo = ref(null); const showDialogEvaluacion = ref(false); @@ -192,7 +193,9 @@ const guardarEvaluacion = async () => { toast.add({ severity: 'warn', summary: 'Debes llenar la evaluación', detail: 'Selecciona una asesoría antes de guardar', life: 3000 }); return; } - + + isSavingEval.value = true; + try { await updateAsesoria(selectedAsesoria.value, { comment: comentarioAsesoria.value, rating: ratingAsesoria.value, @@ -209,6 +212,7 @@ const guardarEvaluacion = async () => { ratingAsesoria.value = null; comentarioAsesoria.value = ''; selectedAsesoria.value = null; + showDialogEvaluacion.value = false; evalInfo.value = await getAsesoriasByUidAndRating(userInfo.value.uid); toast.add({ severity: 'success', @@ -216,7 +220,12 @@ const guardarEvaluacion = async () => { detail: 'La evaluación se registró con éxito', life: 3000, }); - + } catch (error) { + console.error("Error al guardar evaluación:", error); + toast.add({ severity: 'error', summary: 'Error', detail: 'Ocurrió un error al guardar la evaluación: ' + error.message, life: 5000 }); + } finally { + isSavingEval.value = false; + } }; @@ -456,21 +465,20 @@ const guardarEvaluacion = async () => {

Sin asesorías para evaluar

- +
+
diff --git a/src/views/Leaderboard.vue b/src/views/Leaderboard.vue index d89bcfe88..5d7c23da9 100644 --- a/src/views/Leaderboard.vue +++ b/src/views/Leaderboard.vue @@ -1,17 +1,34 @@ @@ -943,6 +988,14 @@ const guardarEvaluacion = async () => { Horarios mentoring icon + @@ -988,21 +1041,51 @@ const guardarEvaluacion = async () => {

Sin asesorías para evaluar

-