feat: org and course student analytics page (#509)

* UI initialization

* feat: structure API

* feat: audience & people analytics

* feat: quick changes

* feat: add comments

* redesign course student page

* chore: make requests to db more efficient

* clean up analytics page for course

* feat: finish up audience analytics page

* chore: minor UI tweaks

* translate all

* replace inserts of log in events to update

* catch error if last_login isn't inserted

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2024-12-30 02:59:10 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 381776f388
commit f69bf920a9
46 changed files with 1916 additions and 437 deletions
@@ -0,0 +1,56 @@
import { type ClassValue, clsx } from 'clsx';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
@@ -10,5 +10,9 @@ export const calDateDiff = (date: string | number | Date): string => {
export const getGreeting = () => {
const date = new Date();
const hours = date.getHours();
return hours < 12 ? 'dashboard.morning_heading' : hours < 18 ? 'dashboard.afternoon_heading' : 'dashboard.evening_heading';
return hours < 12
? 'dashboard.morning_heading'
: hours < 18
? 'dashboard.afternoon_heading'
: 'dashboard.evening_heading';
};
@@ -1,5 +1,5 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { config } from '$lib/config';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
export let supabase: SupabaseClient;
@@ -22,3 +22,8 @@ export const isSupabaseTokenInLocalStorage = () => {
return false;
};
export const getAccessToken = async () => {
const { data } = await getSupabase().auth.getSession();
return data.session?.access_token || '';
};
@@ -1,17 +1,10 @@
import { LOCALE } from '$lib/utils/types';
import i18n from '@sveltekit-i18n/base';
import parser from '@sveltekit-i18n/parser-icu';
import { writable } from 'svelte/store';
import i18n, { type Config } from 'sveltekit-i18n';
interface Params {
dateValue: number;
value: number;
download: number;
award: number;
val: Date;
}
export const config: Config<Partial<Params>> = {
// parser: parser(),
export const config = {
parser: parser(),
loaders: [
{
locale: 'en',
@@ -253,6 +253,17 @@ export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId });
}
export async function getMarks(courseId) {
if (!courseId) return;
// Gets courses for a particular organisation where the current logged in user is a groupmember
const { data: marks } = await supabase
.rpc('get_marks')
.eq('course_id', courseId);
return { marks };
}
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
@@ -1,4 +1,4 @@
import { supabase } from '$lib/utils/functions/supabase';
import { getAccessToken } from '$lib/utils/functions/supabase';
export const NOTIFICATION_NAME = {
WELCOME_TO_APP: 'WELCOME TO APP',
@@ -29,11 +29,6 @@ const NAME_TO_PATH = {
[NOTIFICATION_NAME.NEWSFEED]: '/api/email/course/newsfeed'
};
const getAccessToken = async () => {
const { data } = await supabase.auth.getSession();
return data.session?.access_token || '';
};
export const triggerSendEmail = async (name: string, body: { [k: string]: unknown }) => {
try {
const accessToken = await getAccessToken();
@@ -480,7 +480,9 @@
"invite_sent": "Einladung gesendet",
"you": "Du",
"remove": "Entfernen"
}
},
"view": "Sicht",
"delete_profile": "Profil löschen"
},
"marks": {
"title": "Markierungen",
@@ -1478,5 +1480,29 @@
"content": "Bist du sicher?",
"no": "NEIN",
"yes": "Ja"
},
"analytics": {
"courses": "Kurse",
"progress": "Fortschritt",
"progress_description": "{value, number} von {total, number} abgeschlossen",
"incomplete": "Unvollständig",
"completed": "Vollendet",
"complete": "vollständig",
"last_seen": "Zuletzt gesehen",
"a_while_ago": "vor einiger Zeit",
"exercises": "Übungen",
"graded": "Benotet",
"not_graded": "Nicht benotet",
"enrolled_courses": "Eingeschriebene Kurse",
"enrolled_courses_description": "Alle eingeschriebenen Kurse",
"overall_course_progress": "Gesamtkursfortschritt",
"overall_course_progress_description": "Prozentsatz der abgeschlossenen Lektionen",
"total_average_grade": "Gesamtdurchschnittsnote",
"total_average_grade_description": "Basierend auf den Übungsnoten im gesamten Kurs",
"overall_course_progress_user_description": "Basierend auf dem Abschluss der Lektion",
"assignment_completion": "Abschluss der Aufgabe",
"assignment_completion_description": "Prozentsatz der abgeschlossenen Aufgaben",
"average_grade": "Durchschnittsnote",
"average_grade_description": "Durchschnittsnote aller abgeschlossenen Aufgaben"
}
}
@@ -889,6 +889,8 @@
"role": "Role",
"action": "Action",
"feedback": "Copied Email to clipboard",
"view": "View",
"delete_profile": "Delete profile",
"invite_modal": {
"title": "Invite people",
"invite": "Invite Tutors",
@@ -1185,6 +1187,31 @@
"upgrade": "Upgrade your plan to invite more students"
},
"analytics": {
"courses": "Courses",
"progress": "Progress",
"progress_description": "{value, number} of {total, number} complete",
"incomplete": "Incomplete",
"completed": "Completed",
"complete": "complete",
"last_seen": "Last seen",
"a_while_ago": "a while ago",
"exercises": "Exercises",
"graded": "Graded",
"not_graded": "Not Graded",
"enrolled_courses": "Enrolled Courses",
"enrolled_courses_description": "All enrolled courses",
"overall_course_progress": "Overall Course Progress",
"overall_course_progress_description": "Percentage of completed lessons",
"total_average_grade": "Total Average Grade",
"total_average_grade_description": "Based on exercise grades across all course",
"overall_course_progress_user_description": "Based on lesson completion",
"assignment_completion": "Assignment Completion",
"assignment_completion_description": "Percentage of completed assignments",
"average_grade": "Average Grade",
"average_grade_description": "Average grade of all completed assignments"
},
"upgrade": {
"certificate": "Upgrade your plan to generate certificates",
"team": "Upgrade your plan to add team members",
@@ -480,7 +480,9 @@
"invite_sent": "Invitar enviado",
"you": "Tú",
"remove": "Eliminar"
}
},
"view": "Vista",
"delete_profile": "Eliminar perfil"
},
"marks": {
"title": "Marcas",
@@ -1478,5 +1480,29 @@
"content": "¿Está seguro?",
"no": "No",
"yes": "Sí"
},
"analytics": {
"courses": "Cursos",
"progress": "Progreso",
"progress_description": "{valor, número} de {total, número} completo",
"incomplete": "Incompleto",
"completed": "Terminado",
"complete": "completo",
"last_seen": "visto por última vez",
"a_while_ago": "hace un tiempo",
"exercises": "Ceremonias",
"graded": "Calificado",
"not_graded": "No calificado",
"enrolled_courses": "Cursos inscritos",
"enrolled_courses_description": "Todos los cursos inscritos",
"overall_course_progress": "Progreso general del curso",
"overall_course_progress_description": "Porcentaje de lecciones completadas",
"total_average_grade": "Calificación promedio total",
"total_average_grade_description": "Basado en calificaciones de ejercicios en todo el curso.",
"overall_course_progress_user_description": "Basado en la finalización de la lección",
"assignment_completion": "Finalización de la tarea",
"assignment_completion_description": "Porcentaje de tareas completadas",
"average_grade": "Calificación promedio",
"average_grade_description": "Calificación promedio de todas las tareas completadas"
}
}
@@ -480,7 +480,9 @@
"invite_sent": "Invitation envoyée",
"you": "Toi",
"remove": "Retirer"
}
},
"view": "Voir",
"delete_profile": "Supprimer le profil"
},
"marks": {
"title": "Des marques",
@@ -1478,5 +1480,29 @@
"content": "Es-tu sûr?",
"no": "Non",
"yes": "Oui"
},
"analytics": {
"courses": "Cours",
"progress": "Progrès",
"progress_description": "{value, number} sur {total, number} terminé",
"incomplete": "Incomplet",
"completed": "Complété",
"complete": "complet",
"last_seen": "Vu pour la dernière fois",
"a_while_ago": "il y a quelque temps",
"exercises": "Exercices",
"graded": "Noté",
"not_graded": "Non noté",
"enrolled_courses": "Cours inscrits",
"enrolled_courses_description": "Tous les cours inscrits",
"overall_course_progress": "Progression globale du cours",
"overall_course_progress_description": "Pourcentage de leçons terminées",
"total_average_grade": "Note moyenne totale",
"total_average_grade_description": "Basé sur les notes des exercices dans tous les cours",
"overall_course_progress_user_description": "Basé sur l'achèvement de la leçon",
"assignment_completion": "Achèvement de la mission",
"assignment_completion_description": "Pourcentage de missions terminées",
"average_grade": "Note moyenne",
"average_grade_description": "Note moyenne de tous les devoirs terminés"
}
}
@@ -480,7 +480,9 @@
"invite_sent": "आमंत्रण भेज दिया",
"you": "आप",
"remove": "निकालना"
}
},
"view": "देखना",
"delete_profile": "प्रोफ़ाइल हटाएं"
},
"marks": {
"title": "निशान",
@@ -1478,5 +1480,29 @@
"content": "क्या आपको यकीन है?",
"no": "नहीं",
"yes": "हाँ"
},
"analytics": {
"courses": "पाठ्यक्रम",
"progress": "प्रगति",
"progress_description": "{value, number} का {total, number} पूर्ण",
"incomplete": "अधूरा",
"completed": "पुरा होना।",
"complete": "पूरा",
"last_seen": "अंतिम बार देखा गया",
"a_while_ago": "कुछ देर पहले",
"exercises": "अभ्यास",
"graded": "ग्रेड दिया गया",
"not_graded": "वर्गीकृत नहीं किया",
"enrolled_courses": "नामांकित पाठ्यक्रम",
"enrolled_courses_description": "सभी नामांकित पाठ्यक्रम",
"overall_course_progress": "समग्र पाठ्यक्रम प्रगति",
"overall_course_progress_description": "पूर्ण किए गए पाठों का प्रतिशत",
"total_average_grade": "कुल औसत ग्रेड",
"total_average_grade_description": "संपूर्ण पाठ्यक्रम में व्यायाम ग्रेड के आधार पर",
"overall_course_progress_user_description": "पाठ पूरा होने पर आधारित",
"assignment_completion": "असाइनमेंट पूरा करना",
"assignment_completion_description": "पूर्ण किये गये कार्यों का प्रतिशत",
"average_grade": "औसत क्षेणी",
"average_grade_description": "सभी पूर्ण किए गए असाइनमेंट का औसत ग्रेड"
}
}
@@ -480,7 +480,9 @@
"invite_sent": "Convite enviado",
"you": "Você",
"remove": "Remover"
}
},
"view": "Visualizar",
"delete_profile": "Excluir perfil"
},
"marks": {
"title": "Marcas",
@@ -1478,5 +1480,29 @@
"content": "Tem certeza?",
"no": "Não",
"yes": "Sim"
},
"analytics": {
"courses": "Cursos",
"progress": "Progresso",
"progress_description": "{value, number} de {total, number} concluído",
"incomplete": "Incompleto",
"completed": "Concluído",
"complete": "completo",
"last_seen": "Visto pela última vez",
"a_while_ago": "há um tempo atrás",
"exercises": "Exercícios",
"graded": "Classificado",
"not_graded": "Não classificado",
"enrolled_courses": "Cursos Inscritos",
"enrolled_courses_description": "Todos os cursos inscritos",
"overall_course_progress": "Progresso geral do curso",
"overall_course_progress_description": "Porcentagem de aulas concluídas",
"total_average_grade": "Nota média total",
"total_average_grade_description": "Com base nas notas dos exercícios em todo o curso",
"overall_course_progress_user_description": "Com base na conclusão da lição",
"assignment_completion": "Conclusão da tarefa",
"assignment_completion_description": "Porcentagem de tarefas concluídas",
"average_grade": "Nota Média",
"average_grade_description": "Nota média de todas as tarefas concluídas"
}
}
@@ -480,7 +480,9 @@
"invite_sent": "Приглашение отправлено",
"you": "Ты",
"remove": "Удалять"
}
},
"view": "Вид",
"delete_profile": "Удалить профиль"
},
"marks": {
"title": "Метки",
@@ -1478,5 +1480,29 @@
"content": "Вы уверены?",
"no": "Нет",
"yes": "Да"
},
"analytics": {
"courses": "Курсы",
"progress": "Прогресс",
"progress_description": "{value, number} из {total, number} завершено",
"incomplete": "Неполный",
"completed": "Завершенный",
"complete": "полный",
"last_seen": "Последний раз видели",
"a_while_ago": "некоторое время назад",
"exercises": "Упражнения",
"graded": "Оцененный",
"not_graded": "Не оценено",
"enrolled_courses": "Зарегистрированные курсы",
"enrolled_courses_description": "Все зарегистрированные курсы",
"overall_course_progress": "Общий прогресс курса",
"overall_course_progress_description": "Процент завершенных уроков",
"total_average_grade": "Общая средняя оценка",
"total_average_grade_description": "На основе оценок за упражнения по всему курсу",
"overall_course_progress_user_description": "По итогам урока",
"assignment_completion": "Выполнение задания",
"assignment_completion_description": "Процент выполненных заданий",
"average_grade": "Средняя оценка",
"average_grade_description": "Средняя оценка за все выполненные задания"
}
}
@@ -480,7 +480,9 @@
"invite_sent": "Lời mời đã gửi",
"you": "Bạn",
"remove": "Di dời"
}
},
"view": "Xem",
"delete_profile": "Xóa hồ sơ"
},
"marks": {
"title": "Điểm",
@@ -1478,5 +1480,29 @@
"content": "Bạn có chắc không?",
"no": "KHÔNG",
"yes": "Đúng"
},
"analytics": {
"courses": "Khóa học",
"progress": "Tiến triển",
"progress_description": "{value, number} của {total, number} đã hoàn thành",
"incomplete": "Chưa hoàn thành",
"completed": "Hoàn thành",
"complete": "hoàn thành",
"last_seen": "Nhìn thấy lần cuối",
"a_while_ago": "cách đây một thời gian",
"exercises": "Bài tập",
"graded": "Đã chấm điểm",
"not_graded": "Không được chấm điểm",
"enrolled_courses": "Các khóa học đã đăng ký",
"enrolled_courses_description": "Tất cả các khóa học đã đăng ký",
"overall_course_progress": "Tiến độ chung của khóa học",
"overall_course_progress_description": "Tỷ lệ bài học đã hoàn thành",
"total_average_grade": "Tổng điểm trung bình",
"total_average_grade_description": "Dựa trên điểm bài tập của tất cả các khóa học",
"overall_course_progress_user_description": "Căn cứ vào việc hoàn thành bài học",
"assignment_completion": "Hoàn thành bài tập",
"assignment_completion_description": "Tỷ lệ bài tập đã hoàn thành",
"average_grade": "Điểm trung bình",
"average_grade_description": "Điểm trung bình của tất cả các bài tập đã hoàn thành"
}
}
@@ -0,0 +1,100 @@
interface User {
id: string;
fullName: string;
avatarUrl: string;
email: string;
lastSeen: string | undefined;
}
export interface UserCourseWithStats {
id: string;
org_id: string;
title: string;
slug: string;
description: string;
logo: string;
banner_image: string;
cost: number;
currency: string;
is_published: boolean;
total_lessons: number;
total_students: number;
progress_rate: number;
progress_percentage: number;
type: string;
member_profile_id: string;
lessons_count: number;
lessons_completed: number;
exercises_count: number;
exercises_completed: number;
average_grade: number;
}
// interface LessonData {
// id: string;
// title: string;
// completed: boolean;
// exerciseNo: number;
// created_at: any;
// }
// interface Metric {
// lesson: {
// total: number;
// completed: number;
// };
// exercise: {
// total: number;
// completed: number;
// averageGrade: number;
// };
// progressPercentage: number;
// }
export interface UserAnalytics {
user: User;
courses: UserCourseWithStats[];
overallCourseProgress: number;
overallAverageGrade: number;
}
export interface UserCourseAnalytics {
user: User;
averageGrade: number;
userExercisesStats: UserExercisesStats[];
totalExercises: number;
completedExercises: number;
// pendingLessons: LessonData[];
// completedLessons: LessonData[];
progressPercentage: number;
}
export interface UserExercisesStats {
id: string;
lessonId: string;
lessonTitle: string;
title: string;
status: number | string | undefined;
score: number;
totalPoints: number;
isCompleted: boolean;
}
export interface UserExerciseStatsQuery {
lesson: {
title: string;
exercise: {
id: string;
title: string;
lesson_id: string;
created_at: string;
question: { points: number }[];
submission: {
groupmember: { id: string; profile_id: string };
total: number;
id: string;
status_id: string;
}[];
}[];
}[];
}
@@ -421,3 +421,15 @@ interface Tabs {
id: number;
name: string;
}
export interface StudentStat {
username: string;
progressPercentage: number;
pendingCourses: Course[];
completedCourses: Course[];
totalLessons: number;
totalLessonsCompleted: number;
totalExercise: number;
totalExercisesCompleted: number;
totalLmsEntry: number;
}