feat: organisation home page with analytics (#532)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
interface Activity {
|
||||
icon: any;
|
||||
title: string;
|
||||
percentage: number;
|
||||
percentage: number | string;
|
||||
description: string;
|
||||
hidePercentage?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script>
|
||||
export let color = 'currentColor';
|
||||
export let width = '18';
|
||||
export let height = '18';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -10,6 +16,6 @@
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15 1.5V3C15.9946 3 16.9484 3.39509 17.6516 4.09835C18.3549 4.80161 18.75 5.75544 18.75 6.75C18.75 7.74456 18.3549 8.69839 17.6516 9.40165C16.9484 10.1049 15.9946 10.5 15 10.5V12C16.3924 12 17.7277 11.4469 18.7123 10.4623C19.6969 9.47774 20.25 8.14239 20.25 6.75C20.25 5.35761 19.6969 4.02226 18.7123 3.03769C17.7277 2.05312 16.3924 1.5 15 1.5ZM15 22.5H16.5V18.75C16.4984 17.3581 15.9447 16.0237 14.9605 15.0395C13.9763 14.0553 12.6419 13.5016 11.25 13.5H6.75C5.35811 13.5016 4.0237 14.0553 3.03949 15.0395C2.05527 16.0237 1.50163 17.3581 1.5 18.75V22.5H3V18.75C3.00117 17.7558 3.39664 16.8027 4.09964 16.0996C4.80265 15.3966 5.7558 15.0012 6.75 15H11.25C12.2442 15.0012 13.1973 15.3966 13.9004 16.0996C14.6034 16.8027 14.9988 17.7558 15 18.75V22.5ZM21 22.5H22.5V18.75C22.4985 17.3581 21.9448 16.0236 20.9606 15.0394C19.9764 14.0552 18.6419 13.5015 17.25 13.5V15C18.2442 15.0011 19.1974 15.3966 19.9004 16.0996C20.6034 16.8026 20.9989 17.7558 21 18.75V22.5ZM11.0834 3.63199C10.4667 3.21993 9.74168 3 9 3C8.00544 3 7.05161 3.39509 6.34835 4.09835C5.64509 4.80161 5.25 5.75544 5.25 6.75C5.25 7.49168 5.46994 8.2167 5.88199 8.83339C6.29405 9.45007 6.87972 9.93072 7.56494 10.2145C8.25016 10.4984 9.00416 10.5726 9.73159 10.4279C10.459 10.2833 11.1272 9.9261 11.6517 9.40165C12.1761 8.8772 12.5333 8.20902 12.6779 7.48159C12.8226 6.75416 12.7484 6.00016 12.4646 5.31494C12.1807 4.62971 11.7001 4.04404 11.0834 3.63199ZM6.08326 2.38478C6.94662 1.80791 7.96165 1.5 9 1.5C10.3924 1.5 11.7277 2.05312 12.7123 3.03769C13.6969 4.02226 14.25 5.35761 14.25 6.75C14.25 7.78835 13.9421 8.80339 13.3652 9.66674C12.7883 10.5301 11.9684 11.203 11.0091 11.6004C10.0498 11.9977 8.99418 12.1017 7.97578 11.8991C6.95738 11.6966 6.02192 11.1965 5.28769 10.4623C4.55347 9.72808 4.05345 8.79262 3.85088 7.77422C3.64831 6.75582 3.75227 5.70022 4.14963 4.74091C4.54699 3.7816 5.2199 2.96166 6.08326 2.38478Z"
|
||||
fill="currentColor"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,10 +1,12 @@
|
||||
<script>
|
||||
export let color = 'currentColor';
|
||||
export let width = '18';
|
||||
export let height = '18';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Kurs erstellen",
|
||||
"view_site": "Website ansehen",
|
||||
"visit_site": "Besuche die Website",
|
||||
"hero_content": "Vielen Dank für das, was Sie tun ❤️. Sie verändern das Leben von Klassenzimmer zu Klassenzimmer und dank Ihnen ist die Welt ein besserer Ort. 😊",
|
||||
"hero_button": "Bauen Sie weiter 🚀",
|
||||
"your_schedule": "Dein Zeitplan",
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"tomorrow": "Morgen",
|
||||
"no_schedule": "An diesem Tag gibt es keinen Unterricht",
|
||||
"join_call": "Nehmen Sie am Anruf teil",
|
||||
"click_to_join": "Klicken Sie hier, um am Anruf teilzunehmen",
|
||||
"no_call_link": "Für diese Lektion wurde kein Anruflink hinzugefügt. Fragen Sie Ihren Trainer",
|
||||
"current_lesson": "Derzeit lernen",
|
||||
"your_progress": "Dein Fortschritt",
|
||||
"continue": "Weitermachen",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Heute ist ein weiterer Tag, um Sie Ihren Lernzielen näher zu bringen. Geben Sie nicht auf, je mehr Sie lernen, desto besser werden Sie.",
|
||||
"dont": "Gib nicht auf",
|
||||
"lessons_completed": "Lektionen abgeschlossen",
|
||||
"No_courses_started": "Es wurden keine Kurse gestartet"
|
||||
"no_courses_started": "Es wurden keine Kurse gestartet",
|
||||
"revenue": "Einnahmen",
|
||||
"revenue_description": "Ungefährer Umsatz aus Kursverkäufen",
|
||||
"no_of_courses": "Anzahl der Kurse",
|
||||
"no_courses_description": "Innerhalb dieser Organisation erstellte Kurse",
|
||||
"total_students": "Gesamtzahl der Studierenden",
|
||||
"total_students_description": "Basierend auf den Einschreibungen der Studierenden",
|
||||
"top_courses": "Top-Kurse",
|
||||
"students": "Studenten",
|
||||
"completion": "Fertigstellung",
|
||||
"create_first_course": "Erstellen Sie Ihren ersten Kurs",
|
||||
"create_first_course_description": "Beginnen Sie mit der Erstellung von Kursen, um den Kursfortschritt zu verfolgen",
|
||||
"recent_enrollments": "Aktuelle Anmeldungen",
|
||||
"publish_first_course": "Veröffentlichen Sie Ihren ersten Kurs",
|
||||
"publish_first_course_description": "Um Anmeldungen zu verfolgen, veröffentlichen Sie einen Kurs und laden Sie Studierende ein",
|
||||
"publish_course": "Kurs veröffentlichen"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Dein Zeitplan",
|
||||
|
||||
@@ -441,16 +441,6 @@
|
||||
"create_course": "Create Course",
|
||||
"view_site": "View Site",
|
||||
"visit_site": "Visit Website",
|
||||
"hero_content": "Thank you for what you do ❤️. You are changing lives one classroom at a time and thanks to you, the world is a better place. 😊",
|
||||
"hero_button": "Keep Building 🚀",
|
||||
"your_schedule": "Your Schedule",
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"tomorrow": "Tomorrow",
|
||||
"no_schedule": "No lesson on this day",
|
||||
"join_call": "Join call",
|
||||
"click_to_join": "Click to join the call",
|
||||
"no_call_link": "No call link was added for this lesson. Ask your trainer",
|
||||
"current_lesson": "Currently Learning",
|
||||
"your_progress": "Your Progress",
|
||||
"continue": "Continue",
|
||||
@@ -459,7 +449,22 @@
|
||||
"lms_dashboard_hero": "Today is another day to bring you closer to your learning goals. Don’t give up, the more you learn the better you get.",
|
||||
"dont": "Don't give up",
|
||||
"lessons_completed": "Lessons completed",
|
||||
"No_courses_started": "No courses started"
|
||||
"no_courses_started": "No courses started",
|
||||
"revenue": "Revenue",
|
||||
"revenue_description": "Approximate revenue made from course sales",
|
||||
"no_of_courses": "Number of courses",
|
||||
"no_courses_description": "Courses created within this organization",
|
||||
"total_students": "Total students",
|
||||
"total_students_description": "Based on student enrollments",
|
||||
"top_courses": "Top Courses",
|
||||
"students": "students",
|
||||
"completion": "completion",
|
||||
"create_first_course": "Create Your First Course",
|
||||
"create_first_course_description": "Start creating courses to track course progress",
|
||||
"recent_enrollments": "Recent Enrollments",
|
||||
"publish_first_course": "Publish Your First Course",
|
||||
"publish_first_course_description": "To track enrollments, publish a course and invite students",
|
||||
"publish_course": "Publish Course"
|
||||
},
|
||||
|
||||
"ai": {
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Crear curso",
|
||||
"view_site": "Ver sitio",
|
||||
"visit_site": "Visita el sitio web",
|
||||
"hero_content": "Gracias por lo que haces ❤️. Estás cambiando vidas aula a aula y gracias a ti, el mundo es un lugar mejor. 😊",
|
||||
"hero_button": "Sigue construyendo 🚀",
|
||||
"your_schedule": "Tu agenda",
|
||||
"today": "Hoy",
|
||||
"yesterday": "Ayer",
|
||||
"tomorrow": "Mañana",
|
||||
"no_schedule": "No hay lección en este día",
|
||||
"join_call": "Unirse a la llamada",
|
||||
"click_to_join": "Haga clic para unirse a la llamada",
|
||||
"no_call_link": "No se agregó ningún enlace de llamada para esta lección. Pregúntale a tu entrenador",
|
||||
"current_lesson": "Actualmente aprendiendo",
|
||||
"your_progress": "Su progreso",
|
||||
"continue": "Continuar",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Hoy es otro día para acercarte a tus objetivos de aprendizaje. No te rindas, cuanto más aprendas, mejor serás.",
|
||||
"dont": "no te rindas",
|
||||
"lessons_completed": "Lecciones completadas",
|
||||
"No_courses_started": "No se iniciaron cursos"
|
||||
"no_courses_started": "No se iniciaron cursos",
|
||||
"revenue": "Ganancia",
|
||||
"revenue_description": "Ingresos aproximados obtenidos de las ventas de cursos",
|
||||
"no_of_courses": "Número de cursos",
|
||||
"no_courses_description": "Cursos creados dentro de esta organización.",
|
||||
"total_students": "Estudiantes totales",
|
||||
"total_students_description": "Basado en la matrícula de estudiantes",
|
||||
"top_courses": "Cursos principales",
|
||||
"students": "estudiantes",
|
||||
"completion": "terminación",
|
||||
"create_first_course": "Crea tu primer curso",
|
||||
"create_first_course_description": "Comience a crear cursos para realizar un seguimiento del progreso del curso",
|
||||
"recent_enrollments": "Inscripciones recientes",
|
||||
"publish_first_course": "Publica tu primer curso",
|
||||
"publish_first_course_description": "Para realizar un seguimiento de las inscripciones, publicar un curso e invitar a estudiantes",
|
||||
"publish_course": "Publicar curso"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Tu agenda",
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Créer un cours",
|
||||
"view_site": "Voir le site",
|
||||
"visit_site": "Visitez le site Web",
|
||||
"hero_content": "Merci pour ce que vous faites ❤️. Vous changez des vies, une classe à la fois et grâce à vous, le monde est meilleur. 😊",
|
||||
"hero_button": "Continuez à construire 🚀",
|
||||
"your_schedule": "Votre emploi du temps",
|
||||
"today": "Aujourd'hui",
|
||||
"yesterday": "Hier",
|
||||
"tomorrow": "Demain",
|
||||
"no_schedule": "Pas de cours ce jour",
|
||||
"join_call": "Rejoindre l'appel",
|
||||
"click_to_join": "Cliquez pour rejoindre l'appel",
|
||||
"no_call_link": "Aucun lien d'appel n'a été ajouté pour cette leçon. Demandez à votre entraîneur",
|
||||
"current_lesson": "En cours d'apprentissage",
|
||||
"your_progress": "Votre progression",
|
||||
"continue": "Continuer",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Aujourd'hui est un autre jour pour vous rapprocher de vos objectifs d'apprentissage. N'abandonnez pas, plus vous apprenez, mieux vous vous améliorez.",
|
||||
"dont": "N'abandonne pas",
|
||||
"lessons_completed": "Leçons terminées",
|
||||
"No_courses_started": "Aucun cours n'a commencé"
|
||||
"no_courses_started": "Aucun cours n'a commencé",
|
||||
"revenue": "Revenu",
|
||||
"revenue_description": "Revenus approximatifs provenant de la vente de cours",
|
||||
"no_of_courses": "Nombre de cours",
|
||||
"no_courses_description": "Cours créés au sein de cette organisation",
|
||||
"total_students": "Nombre total d'étudiants",
|
||||
"total_students_description": "Basé sur les inscriptions des étudiants",
|
||||
"top_courses": "Meilleurs cours",
|
||||
"students": "étudiants",
|
||||
"completion": "achèvement",
|
||||
"create_first_course": "Créez votre premier cours",
|
||||
"create_first_course_description": "Commencez à créer des cours pour suivre la progression des cours",
|
||||
"recent_enrollments": "Inscriptions récentes",
|
||||
"publish_first_course": "Publiez votre premier cours",
|
||||
"publish_first_course_description": "Pour suivre les inscriptions, publier un cours et inviter des étudiants",
|
||||
"publish_course": "Publier un cours"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Votre emploi du temps",
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "पाठ्यक्रम बनाएं",
|
||||
"view_site": "साइट देखें",
|
||||
"visit_site": "बेवसाइट देखना",
|
||||
"hero_content": "आप जो करते हैं उसके लिए धन्यवाद ❤️। आप एक समय में एक कक्षा में जीवन बदल रहे हैं और आपके लिए धन्यवाद, दुनिया एक बेहतर जगह है। 😊",
|
||||
"hero_button": "निर्माण करते रहें 🚀",
|
||||
"your_schedule": "आपका समय - सारणी",
|
||||
"today": "आज",
|
||||
"yesterday": "कल",
|
||||
"tomorrow": "कल",
|
||||
"no_schedule": "इस दिन कोई पाठ नहीं",
|
||||
"join_call": "कॉल में शामिल हों",
|
||||
"click_to_join": "कॉल में शामिल होने के लिए क्लिक करें",
|
||||
"no_call_link": "इस पाठ के लिए कोई कॉल लिंक नहीं जोड़ा गया था. अपने प्रशिक्षक से पूछें",
|
||||
"current_lesson": "वर्तमान में सीख रहा हूँ",
|
||||
"your_progress": "आपकी प्रगति",
|
||||
"continue": "जारी रखना",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "आज का दिन आपको आपके सीखने के लक्ष्यों के करीब लाने का एक और दिन है। हार मत मानो, जितना अधिक सीखोगे उतना बेहतर हो जाओगे।",
|
||||
"dont": "हिम्मत मत हारो",
|
||||
"lessons_completed": "पाठ पूरा हुआ",
|
||||
"No_courses_started": "कोई पाठ्यक्रम शुरू नहीं हुआ"
|
||||
"no_courses_started": "कोई पाठ्यक्रम शुरू नहीं हुआ",
|
||||
"revenue": "आय",
|
||||
"revenue_description": "पाठ्यक्रम की बिक्री से प्राप्त अनुमानित राजस्व",
|
||||
"no_of_courses": "पाठ्यक्रमों की संख्या",
|
||||
"no_courses_description": "इस संगठन के अंतर्गत बनाए गए पाठ्यक्रम",
|
||||
"total_students": "कुल छात्र",
|
||||
"total_students_description": "छात्र नामांकन के आधार पर",
|
||||
"top_courses": "शीर्ष पाठ्यक्रम",
|
||||
"students": "छात्र",
|
||||
"completion": "समापन",
|
||||
"create_first_course": "अपना पहला कोर्स बनाएं",
|
||||
"create_first_course_description": "पाठ्यक्रम की प्रगति को ट्रैक करने के लिए पाठ्यक्रम बनाना प्रारंभ करें",
|
||||
"recent_enrollments": "हाल के नामांकन",
|
||||
"publish_first_course": "अपना पहला पाठ्यक्रम प्रकाशित करें",
|
||||
"publish_first_course_description": "नामांकन पर नज़र रखने के लिए, एक पाठ्यक्रम प्रकाशित करें और छात्रों को आमंत्रित करें",
|
||||
"publish_course": "पाठ्यक्रम प्रकाशित करें"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "आपका समय - सारणी",
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Criar curso",
|
||||
"view_site": "Ver site",
|
||||
"visit_site": "Visite o site",
|
||||
"hero_content": "Obrigado pelo que você faz ❤️. Você está mudando vidas, uma sala de aula de cada vez e, graças a você, o mundo é um lugar melhor. 😊",
|
||||
"hero_button": "Continue construindo 🚀",
|
||||
"your_schedule": "Sua programação",
|
||||
"today": "Hoje",
|
||||
"yesterday": "Ontem",
|
||||
"tomorrow": "Amanhã",
|
||||
"no_schedule": "Nenhuma lição neste dia",
|
||||
"join_call": "Participar da chamada",
|
||||
"click_to_join": "Clique para participar da chamada",
|
||||
"no_call_link": "Nenhum link de chamada foi adicionado para esta lição. Pergunte ao seu treinador",
|
||||
"current_lesson": "Atualmente aprendendo",
|
||||
"your_progress": "Seu progresso",
|
||||
"continue": "Continuar",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Hoje é mais um dia para aproximar você de seus objetivos de aprendizagem. Não desista, quanto mais você aprende melhor você fica.",
|
||||
"dont": "Não desista",
|
||||
"lessons_completed": "Lições concluídas",
|
||||
"No_courses_started": "Nenhum curso iniciado"
|
||||
"no_courses_started": "Nenhum curso iniciado",
|
||||
"revenue": "Receita",
|
||||
"revenue_description": "Receita aproximada obtida com vendas de cursos",
|
||||
"no_of_courses": "Número de cursos",
|
||||
"no_courses_description": "Cursos criados nesta organização",
|
||||
"total_students": "Total de alunos",
|
||||
"total_students_description": "Com base nas matrículas dos alunos",
|
||||
"top_courses": "Principais cursos",
|
||||
"students": "estudantes",
|
||||
"completion": "conclusão",
|
||||
"create_first_course": "Crie seu primeiro curso",
|
||||
"create_first_course_description": "Comece a criar cursos para acompanhar o progresso do curso",
|
||||
"recent_enrollments": "Inscrições recentes",
|
||||
"publish_first_course": "Publique seu primeiro curso",
|
||||
"publish_first_course_description": "Para rastrear matrículas, publicar um curso e convidar alunos",
|
||||
"publish_course": "Publicar curso"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Sua programação",
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Создать курс",
|
||||
"view_site": "Посмотреть сайт",
|
||||
"visit_site": "Посетите вебсайт",
|
||||
"hero_content": "Спасибо за то, что вы делаете ❤️. Вы меняете жизнь каждого класса за раз, и благодаря вам мир становится лучше. 😊",
|
||||
"hero_button": "Продолжайте строить 🚀",
|
||||
"your_schedule": "Твое расписание",
|
||||
"today": "Сегодня",
|
||||
"yesterday": "Вчера",
|
||||
"tomorrow": "Завтра",
|
||||
"no_schedule": "В этот день уроков не будет",
|
||||
"join_call": "Присоединиться к звонку",
|
||||
"click_to_join": "Нажмите, чтобы присоединиться к звонку",
|
||||
"no_call_link": "Для этого урока не была добавлена ссылка для звонка. Спроси своего тренера",
|
||||
"current_lesson": "В настоящее время учусь",
|
||||
"your_progress": "Ваш прогресс",
|
||||
"continue": "Продолжать",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Сегодня еще один день, который приблизит вас к вашим целям обучения. Не сдавайтесь, чем больше вы узнаете, тем лучше у вас получится.",
|
||||
"dont": "Не сдавайся",
|
||||
"lessons_completed": "Уроки завершены",
|
||||
"No_courses_started": "Курсы не начались"
|
||||
"no_courses_started": "Курсы не начались",
|
||||
"revenue": "Доход",
|
||||
"revenue_description": "Примерный доход от продажи курсов",
|
||||
"no_of_courses": "Количество курсов",
|
||||
"no_courses_description": "Курсы, созданные в этой организации",
|
||||
"total_students": "Всего студентов",
|
||||
"total_students_description": "На основе набора студентов",
|
||||
"top_courses": "Лучшие курсы",
|
||||
"students": "студенты",
|
||||
"completion": "завершение",
|
||||
"create_first_course": "Создайте свой первый курс",
|
||||
"create_first_course_description": "Начните создавать курсы, чтобы отслеживать прогресс курса",
|
||||
"recent_enrollments": "Недавние регистрации",
|
||||
"publish_first_course": "Опубликуйте свой первый курс",
|
||||
"publish_first_course_description": "Чтобы отслеживать зачисления, опубликуйте курс и пригласите студентов.",
|
||||
"publish_course": "Опубликовать курс"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Твое расписание",
|
||||
|
||||
@@ -905,16 +905,6 @@
|
||||
"create_course": "Tạo khóa học",
|
||||
"view_site": "Xem trang web",
|
||||
"visit_site": "Truy cập trang web",
|
||||
"hero_content": "Cảm ơn vì những gì bạn làm ❤️. Bạn đang thay đổi cuộc sống của từng lớp học và nhờ có bạn, thế giới trở thành một nơi tốt đẹp hơn. 😊",
|
||||
"hero_button": "Tiếp tục xây dựng 🚀",
|
||||
"your_schedule": "Lịch trình của bạn",
|
||||
"today": "Hôm nay",
|
||||
"yesterday": "Hôm qua",
|
||||
"tomorrow": "Ngày mai",
|
||||
"no_schedule": "Không có bài học vào ngày này",
|
||||
"join_call": "Tham gia cuộc gọi",
|
||||
"click_to_join": "Bấm để tham gia cuộc gọi",
|
||||
"no_call_link": "Không có liên kết cuộc gọi nào được thêm vào bài học này. Hỏi huấn luyện viên của bạn",
|
||||
"current_lesson": "Hiện đang học",
|
||||
"your_progress": "Sự tiến bộ của bạn",
|
||||
"continue": "Tiếp tục",
|
||||
@@ -923,7 +913,22 @@
|
||||
"lms_dashboard_hero": "Hôm nay là một ngày khác để đưa bạn đến gần hơn với mục tiêu học tập của mình. Đừng bỏ cuộc, bạn càng học nhiều thì bạn càng tiến bộ hơn.",
|
||||
"dont": "Đừng bỏ cuộc",
|
||||
"lessons_completed": "Bài học đã hoàn thành",
|
||||
"No_courses_started": "Không có khóa học nào bắt đầu"
|
||||
"no_courses_started": "Không có khóa học nào bắt đầu",
|
||||
"revenue": "Doanh thu",
|
||||
"revenue_description": "Doanh thu ước tính từ việc bán khóa học",
|
||||
"no_of_courses": "Số lượng khóa học",
|
||||
"no_courses_description": "Các khóa học được tạo trong tổ chức này",
|
||||
"total_students": "Tổng số học sinh",
|
||||
"total_students_description": "Căn cứ vào số lượng tuyển sinh",
|
||||
"top_courses": "Các khóa học hàng đầu",
|
||||
"students": "sinh viên",
|
||||
"completion": "hoàn thành",
|
||||
"create_first_course": "Tạo khóa học đầu tiên của bạn",
|
||||
"create_first_course_description": "Bắt đầu tạo các khóa học để theo dõi tiến độ khóa học",
|
||||
"recent_enrollments": "Đăng ký gần đây",
|
||||
"publish_first_course": "Xuất bản khóa học đầu tiên của bạn",
|
||||
"publish_first_course_description": "Để theo dõi việc đăng ký, xuất bản một khóa học và mời sinh viên",
|
||||
"publish_course": "Xuất bản khóa học"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Lịch trình của bạn",
|
||||
|
||||
@@ -58,6 +58,26 @@ export interface UserAnalytics {
|
||||
overallAverageGrade: number;
|
||||
}
|
||||
|
||||
export interface OrganisationAnalytics {
|
||||
revenue: number;
|
||||
numberOfCourses: number;
|
||||
totalStudents: number;
|
||||
topCourses: {
|
||||
id: string;
|
||||
title: string;
|
||||
enrollments: number;
|
||||
completion: number;
|
||||
}[];
|
||||
enrollments: {
|
||||
id: string;
|
||||
avatarUrl: string;
|
||||
name: string;
|
||||
courseId: string;
|
||||
course: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface UserCourseAnalytics {
|
||||
user: User;
|
||||
averageGrade: number;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
import { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
const CACHE_DURATION = 60 * 5; // 5 minutes
|
||||
|
||||
export async function POST({ setHeaders, request }) {
|
||||
const { orgId } = await request.json();
|
||||
|
||||
const accessToken = request.headers.get('Authorization');
|
||||
|
||||
if (!orgId || !accessToken) {
|
||||
return json({ success: false, message: 'Request is missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
let user;
|
||||
try {
|
||||
const { data } = await supabase.auth.getUser(accessToken);
|
||||
user = data.user;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
|
||||
}
|
||||
|
||||
setHeaders({
|
||||
'cache-control': `max-age=${CACHE_DURATION}`,
|
||||
'content-type': 'application/json'
|
||||
});
|
||||
|
||||
try {
|
||||
const analytics = await getOrganisationAnalytics(orgId);
|
||||
|
||||
return json(analytics, { status: 200 });
|
||||
} catch (error) {
|
||||
return json({ error: 'Something went wrong' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrganisationAnalytics(orgId: string): Promise<OrganisationAnalytics> {
|
||||
const analytics: OrganisationAnalytics = {
|
||||
revenue: 0,
|
||||
numberOfCourses: 0,
|
||||
totalStudents: 0,
|
||||
topCourses: [],
|
||||
enrollments: []
|
||||
};
|
||||
// Run all queries in parallel
|
||||
const [statsResult, topCoursesResult, enrollmentsResult] = await Promise.all([
|
||||
supabase.from('dash_org_stats').select('*').eq('org_id', orgId).single(),
|
||||
|
||||
supabase
|
||||
.rpc('get_dash_org_top_courses', {
|
||||
org_id_arg: orgId
|
||||
})
|
||||
.order('completion_percentage', { ascending: false }),
|
||||
|
||||
supabase.rpc('get_dash_org_recent_enrollments', {
|
||||
org_id_arg: orgId
|
||||
})
|
||||
]);
|
||||
|
||||
// Check for errors
|
||||
if (statsResult.error) {
|
||||
console.error(statsResult.error);
|
||||
throw new Error('Failed to fetch organisation analytics');
|
||||
}
|
||||
if (topCoursesResult.error) {
|
||||
console.error(topCoursesResult.error);
|
||||
throw new Error('Failed to fetch top courses');
|
||||
}
|
||||
if (enrollmentsResult.error) {
|
||||
console.error(enrollmentsResult.error);
|
||||
throw new Error('Failed to fetch enrollments');
|
||||
}
|
||||
|
||||
// Set analytics data
|
||||
analytics.revenue = 0; // Hardcode until we implement payment
|
||||
analytics.numberOfCourses = statsResult.data.no_of_courses;
|
||||
analytics.totalStudents = statsResult.data.enrolled_students;
|
||||
|
||||
analytics.topCourses = topCoursesResult.data.map((course) => ({
|
||||
id: course.course_id,
|
||||
title: course.course_title,
|
||||
enrollments: course.total_students,
|
||||
completion: course.completion_percentage
|
||||
}));
|
||||
|
||||
analytics.enrollments = enrollmentsResult.data.map((enrollment) => ({
|
||||
id: enrollment.profile_id,
|
||||
avatarUrl: enrollment.avatar_url,
|
||||
name: enrollment.fullname,
|
||||
courseId: enrollment.course_id,
|
||||
course: enrollment.course_title,
|
||||
date: enrollment.enrolled_at
|
||||
}));
|
||||
|
||||
return analytics;
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Learning from '$lib/components/LMS/components/Learning.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { courseMetaDeta, courses } from '$lib/components/Courses/store';
|
||||
import Learning from '$lib/components/LMS/components/Learning.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { getGreeting } from '$lib/utils/functions/date';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
|
||||
let hasFetched = false;
|
||||
let progressPercentage = 0;
|
||||
@@ -51,10 +51,10 @@
|
||||
<title>Student Dashboard - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="max-w-6xl gap-5 mx-auto">
|
||||
<section class="mx-auto max-w-6xl gap-5">
|
||||
<div class="m-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white md:text-3xl">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
@@ -63,10 +63,10 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full h-fit lg:h-[265px] flex md:items-center justify-between flex-col-reverse md:flex-row p-5 lg:p-10 rounded-md bg-primary-900 my-2"
|
||||
class="bg-primary-900 my-2 flex h-fit w-full flex-col-reverse justify-between rounded-md p-5 md:flex-row md:items-center lg:h-[265px] lg:p-10"
|
||||
>
|
||||
<div class="w-full md:w-[75%] lg:w-[80%]">
|
||||
<p class=" text-white text-xs lg:text-xl font-normal mb-5">
|
||||
<p class=" mb-5 text-xs font-normal text-white lg:text-xl">
|
||||
{$currentOrg.customization.dashboard.bannerText
|
||||
? $currentOrg.customization.dashboard.bannerText
|
||||
: $t('dashboard.lms_dashboard_hero')}
|
||||
@@ -82,28 +82,28 @@
|
||||
? $currentOrg.customization.dashboard.bannerImage
|
||||
: '/images/student-learning.svg'}
|
||||
alt="student Learning Pictogram"
|
||||
class="w-28 md:block md:w-1/3 lg:w-[275px] lg:h-[205px] mb-3 md:mb-0"
|
||||
class="mb-3 w-28 md:mb-0 md:block md:w-1/3 lg:h-[205px] lg:w-[275px]"
|
||||
/>
|
||||
</div>
|
||||
<section class="flex w-full flex-col lg:flex-row md:gap-5">
|
||||
<div class="w-full lg:w-[50%] mt-10 xl:mt-2">
|
||||
<section class="flex w-full flex-col md:gap-5 lg:flex-row">
|
||||
<div class="mt-10 w-full lg:w-[50%] xl:mt-2">
|
||||
<Learning />
|
||||
</div>
|
||||
<div class="mt-10 xl:mt-2 w-full lg:w-[50%]">
|
||||
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">
|
||||
<div class="mt-10 w-full lg:w-[50%] xl:mt-2">
|
||||
<p class="pb-3 text-base font-semibold text-[#040F2D] dark:text-white">
|
||||
{$t('dashboard.your_progress')}
|
||||
</p>
|
||||
<div
|
||||
class="flex items-center justify-center border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded h-fit lg:h-[40vh] lg:overflow-y-auto p-3"
|
||||
class="flex h-fit items-center justify-center gap-2 rounded border border-[#EAEAEA] p-3 dark:bg-neutral-800 lg:h-[40vh] lg:overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex flex-col sm:flex-row xl:flex-col items-center justify-between lg:items-center xl:items-start gap-5 lg:justify-around"
|
||||
class="flex h-full w-full flex-col items-center justify-between gap-5 sm:flex-row lg:items-center lg:justify-around xl:flex-col xl:items-start"
|
||||
>
|
||||
<div>
|
||||
<img src="/images/target.svg" alt="student Learning score" />
|
||||
</div>
|
||||
<span class="text-center xl:text-start">
|
||||
<p class="text-base font-semibold py-2 text-[#040F2D] dark:text-white">
|
||||
<p class="py-2 text-base font-semibold text-[#040F2D] dark:text-white">
|
||||
{$t('dashboard.your_progress')}
|
||||
</p>
|
||||
{#if totalLessons > 0}
|
||||
@@ -113,12 +113,12 @@
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-xs font-normal text-[#656565] dark:text-white">
|
||||
{$t('dashboard.No_courses_started')}
|
||||
{$t('dashboard.no_courses_started')}
|
||||
</p>
|
||||
{/if}
|
||||
</span>
|
||||
<h1
|
||||
class="text-5xl lg:text-6xl font-bold text-[#262626] dark:text-white my-0 whitespace-nowrap"
|
||||
class="my-0 whitespace-nowrap text-5xl font-bold text-[#262626] dark:text-white lg:text-6xl"
|
||||
>
|
||||
{progressPercentage} %
|
||||
</h1>
|
||||
|
||||
@@ -1,182 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ActivityCard } from '$lib/components/Analytics';
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import UserProfile from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
||||
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { getAccessToken } from '$lib/utils/functions/supabase';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
|
||||
import UnlockedIcon from 'carbon-icons-svelte/lib/Unlocked.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Datepicker, InlineCalendar } from 'svelte-calendar';
|
||||
// import { supabase } from '$lib/utils/functions/supabase';
|
||||
// import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import Book from 'carbon-icons-svelte/lib/Book.svelte';
|
||||
import CurrencyDollar from 'carbon-icons-svelte/lib/CurrencyDollar.svelte';
|
||||
import UserMultiple from 'carbon-icons-svelte/lib/UserMultiple.svelte';
|
||||
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
|
||||
import { getGreeting } from '$lib/utils/functions/date';
|
||||
import { formatDate, formatUserUpcomingData } from '$lib/utils/functions/routes/dashboard';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fetchUserUpcomingData } from '$lib/utils/services/dashboard';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrg, currentOrgPath, isOrgAdmin, userUpcomingData } from '$lib/utils/store/org';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { user } from '$lib/utils/store/user';
|
||||
import type { LessonsByMonthIndexType, UserLessonDataType } from '$lib/utils/types';
|
||||
import { Grid, Link, SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const { orgName } = data;
|
||||
|
||||
let theme = {};
|
||||
|
||||
let store;
|
||||
let calendar: HTMLElement | null = null;
|
||||
let lessonsByMonth: LessonsByMonthIndexType = {};
|
||||
let prevSelectedDate: string = new Date().toUTCString();
|
||||
let selectedDateLessonData: UserLessonDataType[] | [] = [];
|
||||
|
||||
const boxes = [
|
||||
{
|
||||
label: 'Revenue (NGN)',
|
||||
value: '0.00'
|
||||
},
|
||||
{
|
||||
label: 'Number of sales',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: 'Number of courses',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: 'Total students',
|
||||
value: 0
|
||||
}
|
||||
];
|
||||
let dashAnalytics: OrganisationAnalytics;
|
||||
|
||||
function createCourse() {
|
||||
goto(`/org/${orgName}/courses?create=true`);
|
||||
}
|
||||
function publishCourse() {
|
||||
goto(`/org/${orgName}/courses`);
|
||||
}
|
||||
|
||||
const addDotsToCalendar = (currentMonthIndexInRenderedMonth: number) =>
|
||||
setTimeout(() => {
|
||||
if (!calendar) calendar = document.getElementById('calendar');
|
||||
// console.log(
|
||||
// `currentMonthIndexInRenderedMonth`,
|
||||
// currentMonthIndexInRenderedMonth
|
||||
// );
|
||||
async function fetchDashAnalytics(orgId: string) {
|
||||
if (!orgId) return;
|
||||
|
||||
const renderedMonths =
|
||||
calendar?.querySelectorAll('.contents .container .stage .grid .grid') || [];
|
||||
// console.log(`renderedMonths`, renderedMonths);
|
||||
const currentMonthIndex = new Date($store?.selected).getMonth();
|
||||
const currentMonth = renderedMonths[currentMonthIndexInRenderedMonth];
|
||||
// console.log(`currentMonth`, currentMonth);
|
||||
// All elements for each day in the month - with prev or next month
|
||||
const currentMonthElements =
|
||||
currentMonth?.querySelectorAll<HTMLElement>('a:not(.outsider)') || [];
|
||||
// console.log(`currentMonthElements`, currentMonthElements, '\n\n');
|
||||
const currentMonthData = lessonsByMonth[currentMonthIndex];
|
||||
if (!currentMonthData) return;
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
for (let i in currentMonthElements) {
|
||||
if (!currentMonthElements.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/analytics/dash', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: accessToken
|
||||
},
|
||||
body: JSON.stringify({ orgId })
|
||||
});
|
||||
|
||||
const dayElement = currentMonthElements[i];
|
||||
dayElement.style.position = 'relative';
|
||||
|
||||
// The date is inside the element like <a>2</a>
|
||||
const dateIndex = Number(dayElement.innerHTML);
|
||||
if (!!currentMonthData[dateIndex]) {
|
||||
// console.log(`dateIndex`, dateIndex);
|
||||
// console.log(`lesson data`, currentMonthData[dateIndex]);
|
||||
dayElement.insertAdjacentHTML('beforeend', '<span class="active-day">•</span>');
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error(response);
|
||||
throw new Error('Failed to fetch analytics data');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
async function runFirstThings(currentSession: { id: string } | null, orgId: string) {
|
||||
if (!currentSession || !currentSession.id || !orgId) return;
|
||||
|
||||
if (!$userUpcomingData.length) {
|
||||
$userUpcomingData = await fetchUserUpcomingData(currentSession.id, orgId);
|
||||
}
|
||||
|
||||
lessonsByMonth = formatUserUpcomingData($userUpcomingData);
|
||||
|
||||
// Only for desktop. Only run on mobile when the user clicks on the date
|
||||
if (!$isMobile) {
|
||||
addDotsToCalendar(1);
|
||||
dashAnalytics = (await response.json()) as OrganisationAnalytics;
|
||||
} catch (error) {
|
||||
snackbar.error('Failed to fetch analytics data');
|
||||
}
|
||||
}
|
||||
|
||||
function getDataOfSelectedDate(_selectedDate: string, orgId: string): UserLessonDataType[] | [] {
|
||||
if (!_selectedDate) return [];
|
||||
$: fetchDashAnalytics($currentOrg.id);
|
||||
|
||||
const selectedDate = new Date(_selectedDate).toUTCString();
|
||||
|
||||
const monthIndex = new Date(selectedDate).getMonth();
|
||||
const dateIndex = new Date(selectedDate).getDate();
|
||||
|
||||
// If we changed the month, update the calendar dots
|
||||
const prevMonthIndex = new Date(prevSelectedDate).getMonth();
|
||||
if (prevMonthIndex !== monthIndex) {
|
||||
const currentMonthIndexInRenderedMonth = monthIndex < prevMonthIndex ? 1 : 2;
|
||||
|
||||
addDotsToCalendar(currentMonthIndexInRenderedMonth);
|
||||
$: cards = [
|
||||
{
|
||||
icon: CurrencyDollar,
|
||||
title: `${$t('dashboard.revenue')} ($)`,
|
||||
percentage: dashAnalytics?.revenue ?? 0,
|
||||
description: $t('dashboard.revenue_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: Book,
|
||||
title: $t('dashboard.no_of_courses'),
|
||||
percentage: dashAnalytics?.numberOfCourses ?? 0,
|
||||
description: $t('dashboard.no_courses_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: UserMultiple,
|
||||
title: $t('dashboard.total_students'),
|
||||
percentage: dashAnalytics?.totalStudents ?? 0,
|
||||
description: $t('dashboard.total_students_description'),
|
||||
hidePercentage: true
|
||||
}
|
||||
|
||||
prevSelectedDate = selectedDate;
|
||||
|
||||
const monthLessonData = lessonsByMonth[monthIndex];
|
||||
if (!monthLessonData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dateLessonData = monthLessonData[dateIndex];
|
||||
if (!dateLessonData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return dateLessonData;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Remove default shouldEnlargeDay=true when <DatePicker /> on mobile
|
||||
if ($store && $store.shouldEnlargeDay) {
|
||||
$store.shouldEnlargeDay = false;
|
||||
}
|
||||
});
|
||||
|
||||
// This is here not called onMount because we want to call it as soon as we have the user's `currentSession` value
|
||||
$: runFirstThings($user.currentSession, $currentOrg.id);
|
||||
|
||||
// Every time the date changes
|
||||
$: selectedDateLessonData = getDataOfSelectedDate($store?.selected, $currentOrg.id);
|
||||
|
||||
$: theme = {
|
||||
calendar: {
|
||||
width: '250px',
|
||||
height: '283px',
|
||||
shadow: '0px 0px 5px rgba(0, 0, 0, 0.25)',
|
||||
legend: {
|
||||
height: '30px'
|
||||
},
|
||||
colors: {
|
||||
text: {
|
||||
primary: $globalStore.isDark ? '#eee' : '#333',
|
||||
highlight: '#fff'
|
||||
},
|
||||
background: {
|
||||
primary: $globalStore.isDark ? '#333' : '#fff',
|
||||
highlight: $globalStore.isDark ? '#5829d6' : '#eb7400',
|
||||
hover: $globalStore.isDark ? '#222' : '#eee'
|
||||
},
|
||||
border: $globalStore.isDark ? '#222' : '#eee'
|
||||
}
|
||||
}
|
||||
};
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -186,7 +100,7 @@
|
||||
<WelcomeModal />
|
||||
|
||||
<div class="w-full max-w-5xl px-5 py-10 md:mx-auto">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="mb-3 text-2xl font-bold dark:text-white md:text-3xl">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
@@ -209,194 +123,143 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-primary-900 my-2 flex h-fit w-full flex-col-reverse justify-between rounded-md p-5 md:flex-row md:items-center lg:h-[265px] lg:p-10"
|
||||
>
|
||||
<span>
|
||||
<p class="mb-5 w-full text-xs font-normal text-white md:w-[75%] lg:w-[80%] lg:text-xl">
|
||||
{$t('dashboard.hero_content')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label={$t('dashboard.hero_button')}
|
||||
variant={VARIANTS.CONTAINED_WHITE}
|
||||
onClick={() => goto(`${$currentOrgPath}/courses`)}
|
||||
/>
|
||||
</span>
|
||||
<img
|
||||
src="/images/student-learning.svg"
|
||||
alt="student Learning Pictogram"
|
||||
class="mb-3 w-28 md:mb-0 md:block md:w-1/3 lg:max-h-[205px] lg:w-[200px]"
|
||||
/>
|
||||
<div class="mb-10 flex flex-wrap items-start">
|
||||
<Grid class="px-0" fullWidth>
|
||||
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each cards as card}
|
||||
{#if !dashAnalytics}
|
||||
<SkeletonPlaceholder
|
||||
style="width: 100%; min-width: 300px; height: 10rem;"
|
||||
class="rounded-md"
|
||||
/>
|
||||
{:else}
|
||||
<ActivityCard activity={card} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex items-start flex-wrap">
|
||||
{#each boxes as box}
|
||||
<div
|
||||
class="w-full md:w-[246px] h-[165px] flex flex-col rounded border border-gray-200 dark:border-neutral-600 justify-center px-5 md:mr-5 mb-5"
|
||||
>
|
||||
<p class="dark:text-white mb-2 text-sm">{box.label}</p>
|
||||
<p class="dark:text-white text-xl font-bold">{box.value}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div> -->
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.top_courses')}
|
||||
</h3>
|
||||
|
||||
<div class="w-full">
|
||||
<!-- Your Schedule -->
|
||||
<div class="container w-full xl:w-auto">
|
||||
<p class="mb-4 mt-7 font-bold dark:text-white">{$t('dashboard.your_schedule')}</p>
|
||||
<div
|
||||
class="flex w-full flex-col items-start gap-3 rounded border border-gray-200 px-2 py-5 dark:border-neutral-600 md:flex-row md:px-5"
|
||||
>
|
||||
{#if !$isMobile}
|
||||
<div id="calendar" class="mt-5 flex w-2/5 min-w-[250px] justify-center">
|
||||
<InlineCalendar bind:store {theme} />
|
||||
</div>
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<SkeletonPlaceholder style="width: 100%; height: 40px;" class="rounded-md" />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="header flex w-full items-center">
|
||||
<Datepicker bind:store let:key let:send let:receive>
|
||||
<button
|
||||
class="text-primary-700 rounded-md p-3 text-lg font-bold hover:bg-gray-300"
|
||||
in:receive|local={{ key }}
|
||||
out:send|local={{ key }}
|
||||
on:click={() => {
|
||||
addDotsToCalendar(1);
|
||||
}}
|
||||
>
|
||||
{formatDate($store?.selected)}
|
||||
</button>
|
||||
</Datepicker>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="max-h-[400px] w-full overflow-y-scroll">
|
||||
{#if !$isMobile}
|
||||
<p class="m-5 font-bold dark:text-white">
|
||||
{formatDate($store?.selected)}
|
||||
</p>
|
||||
{/if}
|
||||
{#each selectedDateLessonData as lessonData}
|
||||
<div class="lesson-data flex items-center justify-between p-5">
|
||||
<div class="w-full">
|
||||
<a
|
||||
class="text-black-700 flex items-center text-lg font-bold"
|
||||
href={`/courses/${lessonData.course_id}/lessons/${
|
||||
lessonData.is_unlocked ? lessonData.lesson_id : ''
|
||||
}`}
|
||||
>
|
||||
{lessonData.lesson_title}
|
||||
{#if lessonData.is_unlocked}
|
||||
<span class="success ml-2">
|
||||
<LockedIcon class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else}
|
||||
<UnlockedIcon class="carbon-icon dark:text-white" />
|
||||
{/if}
|
||||
</a>
|
||||
<p class="text-grey flex items-center text-sm dark:text-white">
|
||||
<a class="text-primary-700 my-2 underline" href="/courses/{lessonData.course_id}">
|
||||
{` ${lessonData.course_title}`}
|
||||
</a>
|
||||
{#each dashAnalytics.topCourses as course}
|
||||
<div class="flex items-center">
|
||||
<div class="space-y-1">
|
||||
<Link href={`/courses/${course.id}`}>
|
||||
<p class="text-sm font-medium leading-none">{course.title}</p>
|
||||
</Link>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{course.enrollments}
|
||||
{$t('dashboard.students')}
|
||||
</p>
|
||||
</div>
|
||||
{#if lessonData.is_unlocked}
|
||||
<a
|
||||
class="join-call bg-primary-600 rounded-3xl py-3 font-bold text-white shadow-lg {!lessonData.call_url &&
|
||||
'pointer-events-none cursor-not-allowed opacity-50'}"
|
||||
href={!!lessonData.call_url ? lessonData.call_url : undefined}
|
||||
target="_blank"
|
||||
title={!!lessonData.call_url
|
||||
? $t('dashboard.click_to_join')
|
||||
: $t('dashboard.no_call_link')}
|
||||
>
|
||||
{$t('dashboard.join_call')}
|
||||
</a>
|
||||
{/if}
|
||||
<div class="ml-auto w-2/6">
|
||||
<Progress value={course.completion} />
|
||||
<div class="text-sm font-medium">
|
||||
{course.completion}%
|
||||
{$t('dashboard.completion')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="dark:text-white flex items-center justify-center w-full no-data">
|
||||
{$t('dashboard.no_schedule')}
|
||||
</p>
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<CourseIcon width="30" height="30" />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
{$t('dashboard.create_first_course')}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">
|
||||
{$t('dashboard.create_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={createCourse}
|
||||
label={$t('dashboard.create_course')}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your Activities -->
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.recent_enrollments')}
|
||||
</h3>
|
||||
|
||||
<!-- <div class="w-full">
|
||||
<p class="dark:text-white font-bold mb-7">Your Activities</p>
|
||||
<div
|
||||
class="rounded border border-gray-200 dark:border-neutral-600 md:min-w-[450px] activities-box py-4 px-2 md:px-5 w-full"
|
||||
>
|
||||
{#each activities as activity}
|
||||
<div class="flex mb-3 pb-3 border-b border-gray-200 dark:border-neutral-600">
|
||||
<Avatar src={activity.avatar} name="avatar" />
|
||||
<div class="ml-2 flex flex-col xl:flex-row">
|
||||
<div class="mr-2">
|
||||
<p class="dark:text-white">{activity.name}</p>
|
||||
<p class="dark:text-white mb-2 mt-1 line-clamp-1">
|
||||
{activity.description}
|
||||
</p>
|
||||
<a href={activity.link}>View</a>
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<SkeletonPlaceholder style="width: 100%; height: 40px;" class="rounded-md" />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each dashAnalytics.enrollments as enrollment}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
src={enrollment.avatarUrl}
|
||||
name={enrollment.name}
|
||||
width="w-6"
|
||||
height="h-6"
|
||||
/>
|
||||
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
<p class="text-md font-medium leading-none">{enrollment.name}</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
<span class="italic">
|
||||
{calDateDiff(enrollment.date)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="dark:text-white">{activity.time}</p>
|
||||
<Link href={`/courses/${enrollment.courseId}`}>
|
||||
<p class="text-sm font-medium leading-none">
|
||||
{enrollment.course}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<UserProfile size={24} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
{$t('dashboard.publish_first_course')}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">
|
||||
{$t('dashboard.publish_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={publishCourse}
|
||||
label={$t('dashboard.publish_course')}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-height: 93vh;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 246px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
min-height: 61px;
|
||||
}
|
||||
|
||||
.lesson-data {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.join-call {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-height: 48px;
|
||||
width: 110px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(span.active-day) {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 40%;
|
||||
color: var(--main-primary-color);
|
||||
}
|
||||
|
||||
:global(#calendar .controls .button.label) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
:global(span.active-day) {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,11 +52,12 @@ CREATE OR REPLACE FUNCTION public.insert_login_event_on_user_session_update()
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
IF (NEW.updated_at IS NOT NULL) THEN
|
||||
IF (NEW.updated_at IS NOT NULL AND NEW.updated_at != OLD.updated_at) THEN
|
||||
INSERT INTO public.analytics_login_events (logged_in_at, user_id)
|
||||
VALUES (NEW.updated_at, NEW.user_id)
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET logged_in_at = NEW.updated_at;
|
||||
SET logged_in_at = EXCLUDED.logged_in_at
|
||||
WHERE analytics_login_events.logged_in_at < EXCLUDED.logged_in_at;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
set check_function_bodies = off;
|
||||
|
||||
create or replace view "public"."dash_org_stats" as SELECT gp.organization_id AS org_id,
|
||||
count(DISTINCT course.id) AS no_of_courses,
|
||||
count(DISTINCT gm.profile_id) AS enrolled_students
|
||||
FROM ((course
|
||||
JOIN "group" gp ON ((gp.id = course.group_id)))
|
||||
LEFT JOIN groupmember gm ON (((gm.group_id = gp.id) AND (gm.role_id = 3))))
|
||||
WHERE (course.status = 'ACTIVE'::text)
|
||||
GROUP BY gp.organization_id;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_dash_org_recent_enrollments(org_id_arg uuid)
|
||||
RETURNS TABLE(profile_id uuid, avatar_url text, fullname text, course_id uuid, course_title character varying, enrolled_at timestamp with time zone)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
p.id,
|
||||
p.avatar_url,
|
||||
p.fullname,
|
||||
course.id,
|
||||
course.title,
|
||||
gm.created_at as enrolled_at
|
||||
FROM
|
||||
course
|
||||
JOIN "group" as gp ON gp.id = course.group_id
|
||||
LEFT JOIN groupmember as gm ON gm.group_id = gp.id AND gm.role_id = 3
|
||||
JOIN profile as p ON p.id = gm.profile_id
|
||||
WHERE
|
||||
course.status = 'ACTIVE'
|
||||
AND gp.organization_id = org_id_arg
|
||||
GROUP BY
|
||||
p.id,
|
||||
course.id,
|
||||
course.title,
|
||||
enrolled_at
|
||||
ORDER BY
|
||||
enrolled_at DESC
|
||||
LIMIT 5;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_dash_org_top_courses(org_id_arg uuid)
|
||||
RETURNS TABLE(course_id uuid, course_title character varying, total_students integer, completion_percentage integer)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
course_stats.course_id,
|
||||
course_stats.course_title,
|
||||
course_stats.total_students::integer,
|
||||
CASE
|
||||
WHEN course_stats.total_students * course_stats.total_lessons = 0 THEN 0
|
||||
ELSE ROUND((course_stats.completed_lessons::numeric / (course_stats.total_students * course_stats.total_lessons)) * 100)::integer
|
||||
END as completion_percentage
|
||||
FROM (
|
||||
SELECT
|
||||
c.id as course_id,
|
||||
c.title as course_title,
|
||||
COUNT(DISTINCT gm.id) as total_students,
|
||||
COUNT(DISTINCT l.id) as total_lessons,
|
||||
COUNT(DISTINCT lc.id) FILTER (WHERE lc.is_complete = true) as completed_lessons
|
||||
FROM course c
|
||||
JOIN "group" g ON g.id = c.group_id
|
||||
LEFT JOIN groupmember gm ON gm.group_id = g.id and gm.role_id = 3
|
||||
LEFT JOIN lesson l ON l.course_id = c.id
|
||||
LEFT JOIN lesson_completion lc ON lc.lesson_id = l.id
|
||||
AND lc.profile_id = gm.profile_id
|
||||
WHERE c.status = 'ACTIVE' AND g.organization_id = org_id_arg
|
||||
GROUP BY c.id, c.title
|
||||
) as course_stats
|
||||
LIMIT 5;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
Reference in New Issue
Block a user