-
{item.title}
+
{item.title}
{#if !item.is_unlocked}
diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json
index be506c9b..3298271e 100644
--- a/apps/dashboard/src/lib/utils/translations/de.json
+++ b/apps/dashboard/src/lib/utils/translations/de.json
@@ -861,6 +861,9 @@
"cancel": "Stornieren",
"post": "Post"
}
+ },
+ "analytics": {
+ "title": "Analyse"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Zertifikate",
"nav_landing_page": "Landingpage",
"nav_settings": "Einstellungen",
- "nav_content": "Inhalt"
+ "nav_content": "Inhalt",
+ "nav_analytics": "Analyse"
},
"not_permitted": {
"header": "Keine Erlaubnis",
@@ -1552,11 +1556,27 @@
"assignment_completion": "Abschluss der Aufgabe",
"assignment_completion_description": "Prozentsatz der abgeschlossenen Aufgaben",
"average_grade": "Durchschnittsnote",
- "average_grade_description": "Durchschnittsnote aller abgeschlossenen Aufgaben"
+ "average_grade_description": "Durchschnittsnote aller abgeschlossenen Aufgaben",
+ "students": "Studenten",
+ "lessons": "Unterricht",
+ "tutors": "Tutoren",
+ "avg_grade": "AVG -Note",
+ "completion_rate": "Abschlussrate",
+ "student_progress_overview": "Übersicht über den Fortschritt der Schüler",
+ "student_name": "Schülername",
+ "lessons_completed": "Lektionen abgeschlossen",
+ "exercises_submitted": "Übungen eingereicht",
+ "actions": "Aktionen",
+ "view_details": "Details anzeigen",
+ "no_students_found": "Keine Schüler gefunden",
+ "no_students_description": "In diesem Kurs sind noch keine Studenten eingeschrieben. Die Schüler werden hier erscheinen, sobald sie sich dem Kurs anschließen.",
+ "showing_students": "Zeigen {Start} bis {End} von {Total} -Studenten",
+ "no_analytics_data": "Keine Analysedaten",
+ "no_analytics_description": "Analysedaten für diesen Kurs können nicht geladen werden. Bitte versuchen Sie es später erneut oder wenden Sie sich an Support, wenn das Problem bestehen bleibt."
},
"common": {
"unsaved_changes": {
"message": "Sie haben nicht gerettete Änderungen. Bist du sicher, dass du gehen willst?"
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json
index 262d7070..e7ae78eb 100644
--- a/apps/dashboard/src/lib/utils/translations/en.json
+++ b/apps/dashboard/src/lib/utils/translations/en.json
@@ -535,6 +535,7 @@
"nav_submissions": "Submissions",
"nav_marks": "Marks",
"nav_people": "People",
+ "nav_analytics": "Analytics",
"nav_certificates": "Certificates",
"nav_landing_page": "Landing Page",
"nav_settings": "Settings"
@@ -1147,6 +1148,10 @@
"courses": "courses"
},
+ "analytics": {
+ "title": "Analytics"
+ },
+
"settings": {
"heading": "Settings",
"cover_image": "Cover Image",
@@ -1257,8 +1262,24 @@
"overall_course_progress_user_description": "Based on lesson completion",
"assignment_completion": "Assignment Completion",
"assignment_completion_description": "Percentage of completed assignments",
+ "average_grade_description": "Average grade of all completed assignments",
+ "students": "Students",
+ "lessons": "Lessons",
+ "tutors": "tutors",
+ "avg_grade": "Avg grade",
+ "completion_rate": "Completion rate",
+ "student_progress_overview": "Student Progress Overview",
+ "student_name": "Student Name",
+ "lessons_completed": "Lessons Completed",
+ "exercises_submitted": "Exercises Submitted",
"average_grade": "Average Grade",
- "average_grade_description": "Average grade of all completed assignments"
+ "actions": "Actions",
+ "view_details": "View Details",
+ "no_students_found": "No Students Found",
+ "no_students_description": "There are no students enrolled in this course yet. Students will appear here once they join the course.",
+ "showing_students": "Showing {start} to {end} of {total} students",
+ "no_analytics_data": "No Analytics Data",
+ "no_analytics_description": "Unable to load analytics data for this course. Please try again later or contact support if the issue persists."
},
"upgrade": {
diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json
index 50ced1f0..ed788d2a 100644
--- a/apps/dashboard/src/lib/utils/translations/es.json
+++ b/apps/dashboard/src/lib/utils/translations/es.json
@@ -861,6 +861,9 @@
"cancel": "Cancelar",
"post": "Correo"
}
+ },
+ "analytics": {
+ "title": "Analítica"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Certificados",
"nav_landing_page": "Página de destino",
"nav_settings": "Ajustes",
- "nav_content": "Contenido"
+ "nav_content": "Contenido",
+ "nav_analytics": "Analítica"
},
"not_permitted": {
"header": "Sin autorización",
@@ -1552,11 +1556,27 @@
"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"
+ "average_grade_description": "Calificación promedio de todas las tareas completadas",
+ "students": "Estudiantes",
+ "lessons": "Lecciones",
+ "tutors": "tutores",
+ "avg_grade": "AVG Grado",
+ "completion_rate": "Tasa de finalización",
+ "student_progress_overview": "Descripción general del progreso del estudiante",
+ "student_name": "Nombre de estudiante",
+ "lessons_completed": "Lecciones completadas",
+ "exercises_submitted": "Ejercicios presentados",
+ "actions": "Comportamiento",
+ "view_details": "Ver detalles",
+ "no_students_found": "No se encontraron estudiantes",
+ "no_students_description": "Todavía no hay estudiantes matriculados en este curso. Los estudiantes aparecerán aquí una vez que se unan al curso.",
+ "showing_students": "Mostrando {inicio} a {final} de {Total} Estudiantes",
+ "no_analytics_data": "Sin datos de análisis",
+ "no_analytics_description": "No se puede cargar datos de análisis para este curso. Vuelva a intentarlo más tarde o comuníquese con el soporte si el problema persiste."
},
"common": {
"unsaved_changes": {
"message": "Tienes cambios no salvos. ¿Estás seguro de que quieres irte?"
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json
index 9565a1a5..77766598 100644
--- a/apps/dashboard/src/lib/utils/translations/fr.json
+++ b/apps/dashboard/src/lib/utils/translations/fr.json
@@ -861,6 +861,9 @@
"cancel": "Annuler",
"post": "Poste"
}
+ },
+ "analytics": {
+ "title": "Analytique"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Certificats",
"nav_landing_page": "Page de destination",
"nav_settings": "Paramètres",
- "nav_content": "Contenu"
+ "nav_content": "Contenu",
+ "nav_analytics": "Analytique"
},
"not_permitted": {
"header": "Aucune autorisation",
@@ -1552,7 +1556,23 @@
"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"
+ "average_grade_description": "Note moyenne de tous les devoirs terminés",
+ "students": "Étudiants",
+ "lessons": "Leçons",
+ "tutors": "tuteurs",
+ "avg_grade": "Grade AVG",
+ "completion_rate": "Taux d'achèvement",
+ "student_progress_overview": "Aperçu des progrès des étudiants",
+ "student_name": "Nom de l'étudiant",
+ "lessons_completed": "Leçons terminées",
+ "exercises_submitted": "Exercices soumis",
+ "actions": "Actes",
+ "view_details": "Afficher les détails",
+ "no_students_found": "Aucun étudiant trouvé",
+ "no_students_description": "Il n'y a pas encore d'étudiants inscrits à ce cours. Les étudiants apparaîtront ici une fois qu'ils auront participé au cours.",
+ "showing_students": "Affichage {démarrer} à {fin} des étudiants {totaux}",
+ "no_analytics_data": "Aucune donnée d'analyse",
+ "no_analytics_description": "Impossible de charger des données d'analyse pour ce cours. Veuillez réessayer plus tard ou contacter le support si le problème persiste."
},
"common": {
"unsaved_changes": {
diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json
index 30039776..eff32c8c 100644
--- a/apps/dashboard/src/lib/utils/translations/hi.json
+++ b/apps/dashboard/src/lib/utils/translations/hi.json
@@ -861,6 +861,9 @@
"cancel": "रद्द करना",
"post": "डाक"
}
+ },
+ "analytics": {
+ "title": "एनालिटिक्स"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "प्रमाण पत्र",
"nav_landing_page": "लैंडिंग पृष्ठ",
"nav_settings": "समायोजन",
- "nav_content": "सामग्री"
+ "nav_content": "सामग्री",
+ "nav_analytics": "एनालिटिक्स"
},
"not_permitted": {
"header": "अनुमति नहीं",
@@ -1552,11 +1556,27 @@
"assignment_completion": "असाइनमेंट पूरा करना",
"assignment_completion_description": "पूर्ण किये गये कार्यों का प्रतिशत",
"average_grade": "औसत क्षेणी",
- "average_grade_description": "सभी पूर्ण किए गए असाइनमेंट का औसत ग्रेड"
+ "average_grade_description": "सभी पूर्ण किए गए असाइनमेंट का औसत ग्रेड",
+ "students": "छात्र",
+ "lessons": "पाठ",
+ "tutors": "ट्यूटर्स",
+ "avg_grade": "AVG ग्रेड",
+ "completion_rate": "पूर्णता दर",
+ "student_progress_overview": "छात्र प्रगति अवलोकन",
+ "student_name": "छात्र नाम",
+ "lessons_completed": "सबक पूरा हुआ",
+ "exercises_submitted": "प्रस्तुत अभ्यास",
+ "actions": "कार्रवाई",
+ "view_details": "विवरण देखें",
+ "no_students_found": "कोई छात्र नहीं मिला",
+ "no_students_description": "इस पाठ्यक्रम में अभी तक कोई छात्र नामांकित नहीं हैं। पाठ्यक्रम में शामिल होने के बाद छात्र यहां दिखाई देंगे।",
+ "showing_students": "{TOTAL} {TOTAL} छात्रों के {अंत} को दिखा रहा है",
+ "no_analytics_data": "कोई एनालिटिक्स डेटा नहीं",
+ "no_analytics_description": "इस पाठ्यक्रम के लिए एनालिटिक्स डेटा लोड करने में असमर्थ। यदि समस्या बनी रहती है, तो कृपया बाद में पुनः प्रयास करें या समर्थन से संपर्क करें।"
},
"common": {
"unsaved_changes": {
"message": "आपके पास अनसुना परिवर्तन हैं। क्या आपकी वाकई छोड़ने की इच्छा है?"
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json
index d30dbbc3..584774e5 100644
--- a/apps/dashboard/src/lib/utils/translations/pl.json
+++ b/apps/dashboard/src/lib/utils/translations/pl.json
@@ -331,7 +331,23 @@
"assignment_completion": "Zakończenie przypisania",
"assignment_completion_description": "Procent wypełnionych zadań",
"average_grade": "Średnia ocena",
- "average_grade_description": "Średnia ocena wszystkich zakończonych zadań"
+ "average_grade_description": "Średnia ocena wszystkich zakończonych zadań",
+ "students": "Studenci",
+ "lessons": "Lekcje",
+ "tutors": "Tutorzy",
+ "avg_grade": "Klasa AVG",
+ "completion_rate": "Wskaźnik zakończenia",
+ "student_progress_overview": "Przegląd postępu uczniów",
+ "student_name": "Nazwa ucznia",
+ "lessons_completed": "Ukończone lekcje",
+ "exercises_submitted": "Przedstawione ćwiczenia",
+ "actions": "Działania",
+ "view_details": "Zobacz szczegóły",
+ "no_students_found": "Nie znaleziono uczniów",
+ "no_students_description": "Na ten kurs nie zapisano jeszcze studentów. Studenci pojawią się tutaj, gdy dołączą do kursu.",
+ "showing_students": "Pokazuje {start} do {end} studentów {total}",
+ "no_analytics_data": "Brak danych analitycznych",
+ "no_analytics_description": "Nie można załadować danych analitycznych dla tego kursu. Spróbuj ponownie później lub skontaktuj się z wsparciem, jeśli problem będzie się utrzymywał."
},
"audience": {
"title": "Publiczność",
@@ -1000,6 +1016,9 @@
"cancel": "Anulować",
"post": "Post"
}
+ },
+ "analytics": {
+ "title": "Analityka"
}
},
"navItems": {
@@ -1011,7 +1030,8 @@
"nav_people": "Ludzie",
"nav_certificates": "Certyfikaty",
"nav_landing_page": "Strona docelowa",
- "nav_settings": "Ustawienia"
+ "nav_settings": "Ustawienia",
+ "nav_analytics": "Analityka"
}
},
"courses": {
@@ -1523,5 +1543,10 @@
"resend_in": "Ponownie weryfikacja w",
"seconds": "towary drugiej jakości",
"snackbar_error": "Błąd podczas wysyłania kodu"
+ },
+ "common": {
+ "unsaved_changes": {
+ "message": "Masz niezbawione zmiany. Czy na pewno chcesz odejść?"
+ }
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json
index fd519d36..1be3b7b0 100644
--- a/apps/dashboard/src/lib/utils/translations/pt.json
+++ b/apps/dashboard/src/lib/utils/translations/pt.json
@@ -861,6 +861,9 @@
"cancel": "Cancelar",
"post": "Publicar"
}
+ },
+ "analytics": {
+ "title": "Análise"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Certificados",
"nav_landing_page": "Página inicial",
"nav_settings": "Configurações",
- "nav_content": "Contente"
+ "nav_content": "Contente",
+ "nav_analytics": "Análise"
},
"not_permitted": {
"header": "Sem permissão",
@@ -1552,11 +1556,27 @@
"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"
+ "average_grade_description": "Nota média de todas as tarefas concluídas",
+ "students": "Alunos",
+ "lessons": "Lições",
+ "tutors": "tutores",
+ "avg_grade": "Grau AVG",
+ "completion_rate": "Taxa de conclusão",
+ "student_progress_overview": "Visão geral do progresso do aluno",
+ "student_name": "Nome do aluno",
+ "lessons_completed": "Lições concluídas",
+ "exercises_submitted": "Exercícios enviados",
+ "actions": "Ações",
+ "view_details": "Ver detalhes",
+ "no_students_found": "Nenhum aluno encontrado",
+ "no_students_description": "Ainda não há alunos matriculados neste curso. Os alunos aparecerão aqui quando ingressarem no curso.",
+ "showing_students": "Mostrando {start} para {end} de {total} alunos",
+ "no_analytics_data": "Sem dados de análise",
+ "no_analytics_description": "Não é possível carregar dados de análise para este curso. Tente novamente mais tarde ou entre em contato com o suporte se o problema persistir."
},
"common": {
"unsaved_changes": {
"message": "Você tem mudanças não salvas. Tem certeza de que deseja sair?"
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json
index ea5ed953..c0a13375 100644
--- a/apps/dashboard/src/lib/utils/translations/ru.json
+++ b/apps/dashboard/src/lib/utils/translations/ru.json
@@ -861,6 +861,9 @@
"cancel": "Отмена",
"post": "Почта"
}
+ },
+ "analytics": {
+ "title": "Аналитика"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Сертификаты",
"nav_landing_page": "Целевая страница",
"nav_settings": "Настройки",
- "nav_content": "Содержание"
+ "nav_content": "Содержание",
+ "nav_analytics": "Аналитика"
},
"not_permitted": {
"header": "Нет разрешения",
@@ -1552,7 +1556,23 @@
"assignment_completion": "Выполнение задания",
"assignment_completion_description": "Процент выполненных заданий",
"average_grade": "Средняя оценка",
- "average_grade_description": "Средняя оценка за все выполненные задания"
+ "average_grade_description": "Средняя оценка за все выполненные задания",
+ "students": "Студенты",
+ "lessons": "Уроки",
+ "tutors": "репетиторы",
+ "avg_grade": "AVG Grade",
+ "completion_rate": "Коэффициент завершения",
+ "student_progress_overview": "Студенческий прогресс обзор",
+ "student_name": "Студенческое имя",
+ "lessons_completed": "Уроки завершены",
+ "exercises_submitted": "Упражнения представлены",
+ "actions": "Действия",
+ "view_details": "Просмотреть детали",
+ "no_students_found": "Никаких студентов не нашли",
+ "no_students_description": "В этом курсе еще нет студентов. Студенты появятся здесь, как только они присоединятся к курсу.",
+ "showing_students": "Показ {start} to {end} {otto} Студенты",
+ "no_analytics_data": "Нет аналитических данных",
+ "no_analytics_description": "Невозможно загрузить данные аналитики для этого курса. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой, если проблема сохранится."
},
"common": {
"unsaved_changes": {
diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json
index 8587c999..fdde8929 100644
--- a/apps/dashboard/src/lib/utils/translations/vi.json
+++ b/apps/dashboard/src/lib/utils/translations/vi.json
@@ -861,6 +861,9 @@
"cancel": "Hủy bỏ",
"post": "Bưu kiện"
}
+ },
+ "analytics": {
+ "title": "Phân tích"
}
},
"navItems": {
@@ -872,7 +875,8 @@
"nav_certificates": "Chứng chỉ",
"nav_landing_page": "Trang đích",
"nav_settings": "Cài đặt",
- "nav_content": "Nội dung"
+ "nav_content": "Nội dung",
+ "nav_analytics": "Phân tích"
},
"not_permitted": {
"header": "Không cho phép",
@@ -1552,11 +1556,27 @@
"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"
+ "average_grade_description": "Điểm trung bình của tất cả các bài tập đã hoàn thành",
+ "students": "Sinh viên",
+ "lessons": "Bài học",
+ "tutors": "gia sư",
+ "avg_grade": "Lớp AVG",
+ "completion_rate": "Tỷ lệ hoàn thành",
+ "student_progress_overview": "Tổng quan về tiến trình học sinh",
+ "student_name": "Tên sinh viên",
+ "lessons_completed": "Bài học hoàn thành",
+ "exercises_submitted": "Bài tập được gửi",
+ "actions": "Hành động",
+ "view_details": "Xem chi tiết",
+ "no_students_found": "Không có học sinh tìm thấy",
+ "no_students_description": "Không có sinh viên đăng ký vào khóa học này. Học sinh sẽ xuất hiện ở đây khi họ tham gia khóa học.",
+ "showing_students": "Hiển thị {start} đến {end} của {Total} sinh viên",
+ "no_analytics_data": "Không có dữ liệu phân tích",
+ "no_analytics_description": "Không thể tải dữ liệu phân tích cho khóa học này. Vui lòng thử lại sau hoặc liên hệ với hỗ trợ nếu vấn đề vẫn còn."
},
"common": {
"unsaved_changes": {
"message": "Bạn có những thay đổi chưa được lưu. Bạn có chắc bạn muốn rời đi?"
}
}
-}
+}
\ No newline at end of file
diff --git a/apps/dashboard/src/lib/utils/types/analytics.ts b/apps/dashboard/src/lib/utils/types/analytics.ts
index ac638c2c..c530f0b2 100644
--- a/apps/dashboard/src/lib/utils/types/analytics.ts
+++ b/apps/dashboard/src/lib/utils/types/analytics.ts
@@ -1,3 +1,64 @@
+export interface CourseAnalytics {
+ totalTutors: number;
+ totalStudents: number;
+ totalLessons: number;
+ totalExercises: number;
+ lessonCompletionRate: number;
+ exerciseCompletionRate: number;
+ averageGrade: number;
+ students: StudentOverview[];
+}
+
+export interface StudentOverview {
+ id: string;
+ profile: {
+ fullname: string;
+ email: string;
+ avatar_url: string;
+ };
+ lessonsCompleted: number;
+ totalLessons: number;
+ exercisesSubmitted: number;
+ totalExercises: number;
+ averageGrade: number;
+ lastSeen: string;
+ progressPercentage: number;
+}
+
+export interface StudentDetailModal {
+ student: {
+ profile: {
+ id: string;
+ fullname: string;
+ email: string;
+ avatar_url: string;
+ };
+ overallStats: {
+ totalCourses: number;
+ totalLessonsCompleted: number;
+ totalExercisesSubmitted: number;
+ overallAverageGrade: number;
+ lastSeen: string;
+ };
+ courses: StudentCourseAnalytics[];
+ };
+}
+
+export interface StudentCourseAnalytics {
+ courseId: string;
+ courseTitle: string;
+ courseLogo: string;
+ role: 'student' | 'tutor' | 'admin';
+ lessonsCompleted: number;
+ totalLessons: number;
+ exercisesSubmitted: number;
+ totalExercises: number;
+ averageGrade: number;
+ progressPercentage: number;
+ lastActivity: string;
+ enrollmentDate: string;
+}
+
interface User {
id: string;
fullName: string;
diff --git a/apps/dashboard/src/routes/api/analytics/course/+server.ts b/apps/dashboard/src/routes/api/analytics/course/+server.ts
new file mode 100644
index 00000000..251ffc2c
--- /dev/null
+++ b/apps/dashboard/src/routes/api/analytics/course/+server.ts
@@ -0,0 +1,281 @@
+import type { CourseAnalytics, StudentOverview } from '$lib/utils/types/analytics';
+
+import type { UserExercisesStats } from '$lib/utils/types/analytics';
+import { fetchProfileCourseProgress } from '$lib/utils/services/courses';
+import { getServerSupabase } from '$lib/utils/functions/supabase.server';
+import { json } from '@sveltejs/kit';
+
+const supabase = getServerSupabase();
+
+const CACHE_DURATION = 60 * 5; // 5 minutes
+
+export async function POST({ setHeaders, request }) {
+ const { courseId } = await request.json();
+
+ if (!courseId) {
+ return json({ success: false, message: 'Request is missing required fields' }, { status: 400 });
+ }
+
+ setHeaders({
+ 'cache-control': `max-age=${CACHE_DURATION}`,
+ 'content-type': 'application/json'
+ });
+
+ try {
+ const analytics = await getCourseAnalytics(courseId);
+ return json(analytics, { status: 200 });
+ } catch (error) {
+ console.error('Course analytics error:', error);
+ return json({ error: 'Something went wrong' }, { status: 500 });
+ }
+}
+
+async function getCourseAnalytics(courseId: string): Promise {
+ const analytics: CourseAnalytics = {
+ totalTutors: 0,
+ totalStudents: 0,
+ totalLessons: 0,
+ totalExercises: 0,
+ lessonCompletionRate: 0,
+ exerciseCompletionRate: 0,
+ averageGrade: 0,
+ students: []
+ };
+
+ // Fetch course basic info and group members
+ const { data: courseData, error: courseError } = await supabase
+ .from('course')
+ .select(
+ `
+ id,
+ title,
+ group(
+ members:groupmember(
+ id,
+ role_id,
+ profile_id,
+ profile(
+ id,
+ fullname,
+ email,
+ avatar_url
+ )
+ )
+ ),
+ lessons:lesson(
+ id,
+ title,
+ exercise(id)
+ )
+ `
+ )
+ .eq('id', courseId)
+ .single();
+
+ if (courseError) {
+ throw new Error('Failed to fetch course data');
+ }
+
+ // Count tutors and students
+ const members = (courseData.group as any)?.members || [];
+ analytics.totalTutors = members.filter(
+ (member) => member.role_id === 1 || member.role_id === 2
+ ).length;
+ analytics.totalStudents = members.filter((member) => member.role_id === 3).length;
+
+ // Count lessons and exercises
+ analytics.totalLessons = courseData.lessons?.length || 0;
+ analytics.totalExercises =
+ courseData.lessons?.reduce((total, lesson) => total + (lesson.exercise?.length || 0), 0) || 0;
+
+ // Get student analytics
+ const studentAnalytics = await Promise.all(
+ members
+ .filter((member) => member.role_id === 3)
+ .map((member) => getStudentOverview(courseId, member))
+ );
+
+ analytics.students = studentAnalytics.filter(
+ (student): student is StudentOverview => student !== null
+ );
+
+ // Calculate aggregated metrics
+ if (analytics.students.length > 0) {
+ analytics.lessonCompletionRate = Math.round(
+ analytics.students.reduce((sum, student) => sum + student.progressPercentage, 0) /
+ analytics.students.length
+ );
+
+ analytics.exerciseCompletionRate = Math.round(
+ analytics.students.reduce((sum, student) => {
+ const completionRate =
+ student.totalExercises > 0
+ ? (student.exercisesSubmitted / student.totalExercises) * 100
+ : 0;
+ return sum + completionRate;
+ }, 0) / analytics.students.length
+ );
+
+ analytics.averageGrade = Math.round(
+ analytics.students.reduce((sum, student) => sum + student.averageGrade, 0) /
+ analytics.students.length
+ );
+ }
+
+ return analytics;
+}
+
+async function getLastLogin(userId: string): Promise {
+ try {
+ const { data, error } = await supabase
+ .from('analytics_login_events')
+ .select('logged_in_at')
+ .eq('user_id', userId)
+ .order('logged_in_at', { ascending: false })
+ .limit(1);
+
+ if (error) throw error;
+
+ return data?.[0]?.logged_in_at;
+ } catch (error) {
+ console.error('Error fetching last login:', error);
+ return undefined;
+ }
+}
+
+async function getStudentOverview(courseId: string, member: any): Promise {
+ try {
+ // Use the same service function that the working analytics/user API uses
+ const { data: courseProgressData, error: progressError } = await fetchProfileCourseProgress(
+ courseId,
+ member.profile_id
+ );
+
+ if (progressError) {
+ console.error('Error fetching course progress:', progressError);
+ return null;
+ }
+
+ const courseProgress = courseProgressData?.[0];
+ if (!courseProgress) {
+ console.error('No course progress data found');
+ return null;
+ }
+
+ // Fetch exercise stats using the same method as the people page
+ const userExercisesStats = await fetchUserExercisesStats(courseId, member.profile_id);
+
+ // Calculate exercise completion using the same logic as people page
+ const completedExercises =
+ userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0;
+ const totalExercises = userExercisesStats?.length || 0;
+
+ // Get last login using the same approach as analytics/user API
+ const lastLoginDate = await getLastLogin(member.profile_id);
+
+ // Format last seen
+ let lastSeen = 'Never';
+ if (lastLoginDate) {
+ const lastLogin = new Date(lastLoginDate);
+ const now = new Date();
+ const diffInHours = Math.floor((now.getTime() - lastLogin.getTime()) / (1000 * 60 * 60));
+
+ if (diffInHours < 1) {
+ lastSeen = 'Just now';
+ } else if (diffInHours < 24) {
+ lastSeen = `${diffInHours} hours ago`;
+ } else {
+ const diffInDays = Math.floor(diffInHours / 24);
+ lastSeen = `${diffInDays} days ago`;
+ }
+ }
+
+ // Calculate progress percentage
+ const lessonsCompleted = courseProgress.lessons_completed || 0;
+ const totalLessons = courseProgress.lessons_count || 0;
+ const progressPercentage =
+ totalLessons > 0 ? Math.round((lessonsCompleted / totalLessons) * 100) : 0;
+
+ // Calculate average grade using the same logic as the people page
+ const totalEarnedPoints =
+ userExercisesStats?.reduce((sum, exercise) => sum + exercise.score, 0) || 0;
+ const totalPoints =
+ userExercisesStats?.reduce((sum, exercise) => sum + exercise.totalPoints, 0) || 0;
+ const averageGrade = totalPoints > 0 ? Math.round((totalEarnedPoints / totalPoints) * 100) : 0;
+
+ return {
+ id: member.profile_id,
+ profile: {
+ fullname: member.profile?.fullname || 'Unknown',
+ email: member.profile?.email || '',
+ avatar_url: member.profile?.avatar_url || ''
+ },
+ lessonsCompleted,
+ totalLessons,
+ exercisesSubmitted: completedExercises,
+ totalExercises: totalExercises,
+ averageGrade,
+ lastSeen,
+ progressPercentage
+ };
+ } catch (error) {
+ console.error('Error getting student overview:', error);
+ return null;
+ }
+}
+
+async function fetchUserExercisesStats(
+ courseId: string,
+ userId: string
+): Promise {
+ try {
+ const { data: courseData, error: queryError } = await supabase
+ .from('course')
+ .select(
+ `
+ lesson!inner (
+ title,exercise!inner (
+ id,title,lesson_id,created_at,question (points),
+ submission!left (
+ id,total,status_id,submitted_by,groupmember!inner (id,profile_id)
+ )
+ )
+ )
+ `
+ )
+ .eq('lesson.exercise.submission.groupmember.profile_id', userId)
+ .eq('id', courseId);
+
+ if (queryError) {
+ console.error('Error fetching exercise data:', queryError);
+ return;
+ }
+
+ if (!courseData || courseData.length === 0) {
+ return;
+ }
+
+ const userExercisesStats = courseData[0].lesson.flatMap(
+ (lesson) =>
+ lesson.exercise?.map((exercise) => {
+ const totalPoints = exercise.question?.reduce((sum, q) => sum + (q.points || 0), 0) || 0;
+ const userSubmission = exercise.submission?.[0];
+
+ return {
+ id: exercise.id,
+ lessonId: exercise.lesson_id,
+ lessonTitle: lesson.title,
+ title: exercise.title,
+ status: userSubmission?.status_id,
+ score: userSubmission?.total || 0,
+ totalPoints,
+ isCompleted: !!userSubmission
+ };
+ }) || []
+ );
+
+ return userExercisesStats;
+ } catch (error) {
+ console.error('Unexpected error:', error);
+ }
+}
diff --git a/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte b/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte
new file mode 100644
index 00000000..593e6d5f
--- /dev/null
+++ b/apps/dashboard/src/routes/courses/[id]/analytics/+page.svelte
@@ -0,0 +1,140 @@
+
+
+
+
+
+ {#if isLoading}
+
+ {:else if courseAnalytics}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {$t('analytics.student_progress_overview')}
+
+
+
+
+
+
+
+
+ {:else}
+
+ {/if}
+
+
diff --git a/apps/dashboard/src/routes/courses/[id]/analytics/+page.ts b/apps/dashboard/src/routes/courses/[id]/analytics/+page.ts
new file mode 100644
index 00000000..6daab497
--- /dev/null
+++ b/apps/dashboard/src/routes/courses/[id]/analytics/+page.ts
@@ -0,0 +1,7 @@
+export const load = ({ params }) => {
+ const { id } = params;
+
+ return {
+ courseId: id
+ };
+};
diff --git a/apps/dashboard/src/routes/courses/[id]/people/+layout.svelte b/apps/dashboard/src/routes/courses/[id]/people/+layout.svelte
index 42b7747d..2f8dfc72 100644
--- a/apps/dashboard/src/routes/courses/[id]/people/+layout.svelte
+++ b/apps/dashboard/src/routes/courses/[id]/people/+layout.svelte
@@ -11,10 +11,21 @@
export let data;
+ // Get back URL from query parameters
+ $: backUrl = $page.url.searchParams.get('back');
+
const handleClick = () => {
const url = $page.url.href + '?add=true';
goto(url);
};
+
+ const handleBackNavigation = () => {
+ if (backUrl) {
+ goto(backUrl);
+ } else {
+ goto(`/courses/${data.courseId}/people`);
+ }
+ };
@@ -22,12 +33,7 @@
{#if data.personId}
- {
- goto(`/courses/${data.courseId}/people`);
- }}
- >
+
diff --git a/apps/dashboard/src/routes/courses/[id]/people/[personId]/+page.svelte b/apps/dashboard/src/routes/courses/[id]/people/[personId]/+page.svelte
index c63994ba..33bc4c95 100644
--- a/apps/dashboard/src/routes/courses/[id]/people/[personId]/+page.svelte
+++ b/apps/dashboard/src/routes/courses/[id]/people/[personId]/+page.svelte
@@ -105,7 +105,7 @@
-
+
{$t('analytics.exercises')}
diff --git a/supabase/migrations/20250917235052_course_analytics.sql b/supabase/migrations/20250917235052_course_analytics.sql
new file mode 100644
index 00000000..00cae1ad
--- /dev/null
+++ b/supabase/migrations/20250917235052_course_analytics.sql
@@ -0,0 +1,31 @@
+set check_function_bodies = off;
+
+CREATE OR REPLACE FUNCTION public.get_course_progress(course_id_arg uuid, profile_id_arg uuid)
+ RETURNS TABLE(lessons_count bigint, lessons_completed bigint, exercises_count bigint, exercises_completed bigint)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+RETURN QUERY
+select
+ count(distinct lesson.id) as lessons_count,
+ count(distinct lesson_completion.id) as lessons_completed,
+ count(distinct exercise.id) as exercises_count,
+ count(distinct submission.id) as exercises_completed
+from
+ course
+ join "group" on "group".id = course.group_id
+ join groupmember on groupmember.group_id = course.group_id
+ join profile on profile.id = groupmember.profile_id
+ left join lesson on lesson.course_id = course.id
+ left join lesson_completion on lesson_completion.lesson_id = lesson.id
+ and lesson_completion.is_complete = true
+ and lesson_completion.profile_id = profile.id
+ left join exercise on exercise.lesson_id = lesson.id
+ left join submission on submission.exercise_id = exercise.id
+ and submission.submitted_by = groupmember.id
+where
+ course.id = course_id_arg
+ and profile.id = profile_id_arg;
+END;
+$function$
+;