fix(analytics): show per-student last seen from login and session activity (#674)
* fix(analytics): show per-student last seen from login and session activity Return ISO last-seen timestamps from course analytics instead of pre-formatted strings so the dashboard can format them per student. Batch-fetch last seen for all students and include session activity alongside login events for more accurate, distinct values. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(analytics): destructure first row from getLastLogin query results Promise.all returns an array per Drizzle select; nested destructuring is required so loggedInAt and updatedAt are read from row objects. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
bbdb2e9419
commit
f2d7afc590
@@ -19,6 +19,7 @@ import {
|
||||
} from '@cio/db/queries/group';
|
||||
import {
|
||||
getLastLogin,
|
||||
getLastSeenForUserIds,
|
||||
getLessonsWithCompletion,
|
||||
getProfileCourseProgress,
|
||||
getUserExercisesStats
|
||||
@@ -33,7 +34,7 @@ import { updateExercise, updateExercisesSectionId } from '@cio/db/queries/exerci
|
||||
import { getProfileById } from '@cio/db/queries/auth';
|
||||
import { insertOrganizationMembersOnConflictDoNothing } from '@cio/db/queries/organization';
|
||||
import { annotateCourseContentWithProgression } from './progression';
|
||||
import { buildCourseContent, calcPercentageWithRounding, formatLastSeen, type CourseContent } from './utils';
|
||||
import { buildCourseContent, calcPercentageWithRounding, type CourseContent } from './utils';
|
||||
import { guardCourseTypeTransition } from './public-course-guard';
|
||||
|
||||
const DEFAULT_CONTENT_GROUPING = true;
|
||||
@@ -439,15 +440,19 @@ export async function getCourseAnalytics(courseId: string) {
|
||||
const exercises = course.contentItems.filter((item) => item.type === ContentType.Exercise);
|
||||
|
||||
// Get student analytics
|
||||
const studentProfileIds = students
|
||||
.map((student) => student.profileId)
|
||||
.filter((profileId): profileId is string => profileId !== null);
|
||||
const lastSeenByProfileId = await getLastSeenForUserIds(studentProfileIds);
|
||||
|
||||
const studentAnalytics = await Promise.all(
|
||||
students
|
||||
.filter((student) => student.profileId !== null)
|
||||
.map(async (student) => {
|
||||
try {
|
||||
const [courseProgress, userExercisesStats, lastLoginDate] = await Promise.all([
|
||||
const [courseProgress, userExercisesStats] = await Promise.all([
|
||||
getCourseProgressQuery(courseId, student.profileId!),
|
||||
getUserExercisesStats(courseId, student.profileId!),
|
||||
getLastLogin(student.profileId!)
|
||||
getUserExercisesStats(courseId, student.profileId!)
|
||||
]);
|
||||
|
||||
if (!courseProgress) {
|
||||
@@ -464,6 +469,7 @@ export async function getCourseAnalytics(courseId: string) {
|
||||
const lessonsCompleted = courseProgress.lessonsCompleted || 0;
|
||||
const totalLessons = courseProgress.lessonsCount || 0;
|
||||
const progressPercentage = calcPercentageWithRounding(lessonsCompleted, totalLessons);
|
||||
const lastSeen = lastSeenByProfileId.get(student.profileId!) ?? undefined;
|
||||
|
||||
return {
|
||||
id: student.profileId,
|
||||
@@ -477,7 +483,7 @@ export async function getCourseAnalytics(courseId: string) {
|
||||
exercisesSubmitted: completedExercises,
|
||||
totalExercises,
|
||||
averageGrade,
|
||||
lastSeen: formatLastSeen(lastLoginDate),
|
||||
lastSeen,
|
||||
progressPercentage
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -551,8 +557,7 @@ export async function getUserCourseAnalytics(courseId: string, userId: string) {
|
||||
throw new AppError('User profile not found', ErrorCodes.PROFILE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
// Get last login
|
||||
const lastLoginDate = await getLastLogin(userId);
|
||||
const lastSeen = await getLastLogin(userId);
|
||||
|
||||
// Fetch user exercises stats, lessons with completion, and course progress
|
||||
const [userExercisesStats, lessons, courseProgress] = await Promise.all([
|
||||
@@ -582,7 +587,7 @@ export async function getUserCourseAnalytics(courseId: string, userId: string) {
|
||||
fullName: profile.fullname || '',
|
||||
email: profile.email || '',
|
||||
avatarUrl: profile.avatarUrl || '',
|
||||
lastSeen: formatLastSeen(lastLoginDate)
|
||||
lastSeen: lastSeen || undefined
|
||||
},
|
||||
averageGrade,
|
||||
userExercisesStats,
|
||||
|
||||
Reference in New Issue
Block a user