feat: show all courses available to students (#356)
* working on the fetch explore course * working on the showing the right course * finished the explore courses page * finished the explore courses page * updated the getcourseurl * updated the migration file * text and spacing improvements * update translations --------- Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
@@ -28,3 +28,20 @@ export async function fetchCourses(profileId, orgId) {
|
||||
|
||||
return { allCourses };
|
||||
}
|
||||
|
||||
export async function fetchExploreCourses(profileId, orgId) {
|
||||
if (!orgId || !profileId) return;
|
||||
|
||||
const { data: allCourses } = await supabase.rpc('get_explore_courses', {
|
||||
org_id_arg: orgId,
|
||||
profile_id_arg: profileId
|
||||
});
|
||||
|
||||
if (!Array.isArray(allCourses)) {
|
||||
return {
|
||||
allCourses: []
|
||||
};
|
||||
}
|
||||
|
||||
return { allCourses };
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
export let currency = 'NGN';
|
||||
export let isOnLandingPage = false;
|
||||
export let isLMS = false;
|
||||
export let isExplore = false;
|
||||
export let progressRate = 45;
|
||||
export let type: COURSE_TYPE;
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
}
|
||||
|
||||
function getCourseUrl() {
|
||||
return isOnLandingPage
|
||||
return isOnLandingPage || isExplore
|
||||
? `/course/${slug}`
|
||||
: `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`;
|
||||
}
|
||||
@@ -159,11 +160,11 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-4 border border-gray dark:border-neutral-600 border-b-0 border-l-0 border-r-0 flex justify-between {isLMS &&
|
||||
class="px-4 py-2 border border-gray dark:border-neutral-600 border-b-0 border-l-0 border-r-0 flex justify-between {isLMS &&
|
||||
'items-center'}"
|
||||
>
|
||||
<div>
|
||||
<p class="text-xs pt-2 {!isLMS && 'pl-2'} dark:text-white">
|
||||
<p class="text-xs {!isLMS && 'pl-2'} dark:text-white">
|
||||
{totalLessons}
|
||||
{$t('courses.course_card.lessons_number')}
|
||||
</p>
|
||||
@@ -175,15 +176,17 @@
|
||||
: formatter.format(cost)}</span
|
||||
>
|
||||
{:else if isLMS}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class=" relative bg-[#EAEAEA] w-[50px] h-1">
|
||||
<div
|
||||
style="width:{progressRate}%"
|
||||
class={`absolute top-0 left-0 bg-primary-700 h-full`}
|
||||
/>
|
||||
{#if !isExplore}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class=" relative bg-[#EAEAEA] w-[50px] h-1">
|
||||
<div
|
||||
style="width:{progressRate}%"
|
||||
class={`absolute top-0 left-0 bg-primary-700 h-full`}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-[#656565] dark:text-white">{progressRate}%</p>
|
||||
</div>
|
||||
<p class="text-xs text-[#656565] dark:text-white">{progressRate}%</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Tag type={isPublished ? 'green' : 'cool-gray'}>
|
||||
{#if isPublished}
|
||||
@@ -198,13 +201,15 @@
|
||||
|
||||
{#if isLMS}
|
||||
<PrimaryButton
|
||||
label={$t('courses.course_card.continue_course')}
|
||||
label={isExplore
|
||||
? $t('courses.course_card.learn_more')
|
||||
: $t('courses.course_card.continue_course')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
className="rounded-none"
|
||||
/>
|
||||
{:else if !isOnLandingPage}
|
||||
<div class="flex flex-col justify-between">
|
||||
<p class="text-xs pt-2 pl-2 dark:text-white">
|
||||
<p class="text-xs pl-2 dark:text-white">
|
||||
{totalStudents}
|
||||
{$t('courses.course_card.students')}
|
||||
</p>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
export let courses: Course[] = [];
|
||||
export let emptyTitle = $t('courses.course_card.empty_title');
|
||||
export let emptyDescription = $t('courses.course_card.empty_description');
|
||||
export let isExplore = false;
|
||||
|
||||
function calcProgressRate(progressRate?: number, totalLessons?: number): number {
|
||||
if (!progressRate || !totalLessons) {
|
||||
@@ -83,6 +84,7 @@
|
||||
{#key courseData.id}
|
||||
<Card
|
||||
id={courseData.id}
|
||||
slug={courseData.slug}
|
||||
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
|
||||
title={courseData.title}
|
||||
description={courseData.description}
|
||||
@@ -93,6 +95,7 @@
|
||||
totalLessons={courseData.total_lessons}
|
||||
totalStudents={courseData.total_students}
|
||||
isLMS={$globalStore.isOrgSite}
|
||||
{isExplore}
|
||||
progressRate={calcProgressRate(courseData.progress_rate, courseData.total_lessons)}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte';
|
||||
import LicenseDraft from 'carbon-icons-svelte/lib/LicenseDraft.svelte';
|
||||
import Explore from 'carbon-icons-svelte/lib/Explore.svelte';
|
||||
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
|
||||
import HomeIcon from '$lib/components/Icons/HomeIcon.svelte';
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
@@ -59,6 +60,11 @@
|
||||
show() {
|
||||
return $currentOrg?.customization?.dashboard?.community;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: $t('lms_navigation.explore'),
|
||||
icon: Explore,
|
||||
link: '/lms/explore'
|
||||
}
|
||||
].filter((link) => (link.show ? link.show() : true));
|
||||
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Übung",
|
||||
"community": "Gemeinschaft",
|
||||
"help": "Helfen",
|
||||
"settings": "Einstellungen"
|
||||
"settings": "Einstellungen",
|
||||
"explore": "Erkunden"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Aktualisieren Sie Ihren Plan, um Zertifikate zu generieren",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Aktie",
|
||||
"invite": "Einladen",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
},
|
||||
"get_course": "Holen Sie sich Kurs",
|
||||
"learn_more": "Erfahren Sie mehr"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "Datum erstellt",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "Die Organisation muss 5 oder mehr Zeichen enthalten",
|
||||
"hyphen_rule": "Der Name der Organisation darf nicht mit einem Bindestrich beginnen oder enden"
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "Kein Kurs verfügbar",
|
||||
"empty_description": "Derzeit sind keine veröffentlichten Kurse verfügbar",
|
||||
"heading": "Entdecken Sie unsere Kurse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,6 +428,7 @@
|
||||
"published": "Published",
|
||||
"unpublished": "Unpublished",
|
||||
"continue_course": "Continue Course",
|
||||
"learn_more": "Learn more",
|
||||
"list_view": {
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
@@ -1101,6 +1102,7 @@
|
||||
"my_learning": "My Learning",
|
||||
"exercise": "Exercise",
|
||||
"community": "Community",
|
||||
"explore": "Explore",
|
||||
"help": "Help",
|
||||
"settings": "Settings"
|
||||
},
|
||||
@@ -1125,6 +1127,12 @@
|
||||
"lesson": "Lesson"
|
||||
},
|
||||
|
||||
"explore": {
|
||||
"heading": "Explore our courses",
|
||||
"empty_heading": "No course available",
|
||||
"empty_description": "There are currently no published course available"
|
||||
},
|
||||
|
||||
"snackbar": {
|
||||
"success": "Success",
|
||||
"success_update": "Updated successfully",
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Ejercicio",
|
||||
"community": "Comunidad",
|
||||
"help": "Ayuda",
|
||||
"settings": "Ajustes"
|
||||
"settings": "Ajustes",
|
||||
"explore": "Explorar"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Actualice su plan para generar certificados",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Compartir",
|
||||
"invite": "Invitar",
|
||||
"delete": "Borrar"
|
||||
}
|
||||
},
|
||||
"get_course": "Obtener curso",
|
||||
"learn_more": "Aprende más"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "fecha de creacion",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "La organización debe contener 5 o más caracteres.",
|
||||
"hyphen_rule": "El nombre de la organización no puede comenzar ni terminar con un guión."
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "No hay curso disponible",
|
||||
"empty_description": "Actualmente no hay ningún curso publicado disponible",
|
||||
"heading": "Explora nuestros cursos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Exercice",
|
||||
"community": "Communauté",
|
||||
"help": "Aide",
|
||||
"settings": "Paramètres"
|
||||
"settings": "Paramètres",
|
||||
"explore": "Explorer"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Mettez à niveau votre plan pour générer des certificats",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Partager",
|
||||
"invite": "Inviter",
|
||||
"delete": "Supprimer"
|
||||
}
|
||||
},
|
||||
"get_course": "Obtenir un cours",
|
||||
"learn_more": "Apprendre encore plus"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "date créée",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "L'organisation doit contenir 5 caractères ou plus",
|
||||
"hyphen_rule": "Le nom de l'organisation ne peut pas commencer ou se terminer par un trait d'union"
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "Aucun cours disponible",
|
||||
"empty_description": "Il n'y a actuellement aucun cours publié disponible",
|
||||
"heading": "Découvrez nos cours"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "व्यायाम",
|
||||
"community": "समुदाय",
|
||||
"help": "मदद",
|
||||
"settings": "समायोजन"
|
||||
"settings": "समायोजन",
|
||||
"explore": "अन्वेषण करना"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "प्रमाणपत्र बनाने की अपनी योजना को अपग्रेड करें",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "शेयर करना",
|
||||
"invite": "आमंत्रित करना",
|
||||
"delete": "मिटाना"
|
||||
}
|
||||
},
|
||||
"get_course": "पाठ्यक्रम प्राप्त करें",
|
||||
"learn_more": "और अधिक जानें"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "बनाया गया दिनांक",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "संगठन में 5 या अधिक वर्ण होने चाहिए",
|
||||
"hyphen_rule": "संगठन का नाम हाइफ़न से प्रारंभ या समाप्त नहीं हो सकता"
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "कोई पाठ्यक्रम उपलब्ध नहीं है",
|
||||
"empty_description": "वर्तमान में कोई प्रकाशित पाठ्यक्रम उपलब्ध नहीं है",
|
||||
"heading": "हमारे पाठ्यक्रमों का अन्वेषण करें"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Exercício",
|
||||
"community": "Comunidade",
|
||||
"help": "Ajuda",
|
||||
"settings": "Configurações"
|
||||
"settings": "Configurações",
|
||||
"explore": "Explorar"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Atualize seu plano para gerar certificados",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Compartilhar",
|
||||
"invite": "Convidar",
|
||||
"delete": "Excluir"
|
||||
}
|
||||
},
|
||||
"get_course": "Obter curso",
|
||||
"learn_more": "Saber mais"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "Data Criada",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "A organização deve conter 5 ou mais caracteres",
|
||||
"hyphen_rule": "O nome da organização não pode começar ou terminar com um hífen"
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "Nenhum curso disponível",
|
||||
"empty_description": "Atualmente não há nenhum curso publicado disponível",
|
||||
"heading": "Explore nossos cursos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Упражнение",
|
||||
"community": "Сообщество",
|
||||
"help": "Помощь",
|
||||
"settings": "Настройки"
|
||||
"settings": "Настройки",
|
||||
"explore": "Исследовать"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Обновите свой план для создания сертификатов",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Делиться",
|
||||
"invite": "Приглашать",
|
||||
"delete": "Удалить"
|
||||
}
|
||||
},
|
||||
"get_course": "Получить курс",
|
||||
"learn_more": "Узнать больше"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "Дата создания",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "Организация должна содержать 5 или более символов.",
|
||||
"hyphen_rule": "Название организации не может начинаться или заканчиваться дефисом."
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "Нет доступных курсов",
|
||||
"empty_description": "На данный момент нет доступных опубликованных курсов",
|
||||
"heading": "Ознакомьтесь с нашими курсами"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"exercise": "Bài tập",
|
||||
"community": "Cộng đồng",
|
||||
"help": "Giúp đỡ",
|
||||
"settings": "Cài đặt"
|
||||
"settings": "Cài đặt",
|
||||
"explore": "Khám phá"
|
||||
},
|
||||
"upgrade": {
|
||||
"certificate": "Nâng cấp gói của bạn để tạo chứng chỉ",
|
||||
@@ -843,7 +844,9 @@
|
||||
"share": "Chia sẻ",
|
||||
"invite": "Mời",
|
||||
"delete": "Xóa bỏ"
|
||||
}
|
||||
},
|
||||
"get_course": "Nhận khóa học",
|
||||
"learn_more": "Tìm hiểu thêm"
|
||||
},
|
||||
"course_filter": {
|
||||
"date_created": "Ngày tạo",
|
||||
@@ -1369,5 +1372,10 @@
|
||||
"min_char": "Tổ chức phải chứa 5 ký tự trở lên",
|
||||
"hyphen_rule": "Tên tổ chức không thể bắt đầu hoặc kết thúc bằng dấu gạch nối"
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"empty_heading": "Không có khóa học nào",
|
||||
"empty_description": "Hiện tại chưa có khóa học nào được xuất bản",
|
||||
"heading": "Khám phá các khóa học của chúng tôi"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Search, Dropdown } from 'carbon-components-svelte';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { fetchExploreCourses } from '$lib/components/Courses/api';
|
||||
import Courses from '$lib/components/Courses/index.svelte';
|
||||
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
|
||||
import List from 'carbon-icons-svelte/lib/List.svelte';
|
||||
|
||||
let searchValue = '';
|
||||
let selectedId: string;
|
||||
let filteredExploreCourses: Course[];
|
||||
let hasFetched = false;
|
||||
let exploreCourseList: Course[] = [];
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
if (hasFetched || !userId || !orgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$courseMetaDeta.isLoading = true;
|
||||
|
||||
const coursesResult = await fetchExploreCourses(userId, orgId);
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
|
||||
if (!coursesResult) return;
|
||||
|
||||
exploreCourseList = coursesResult.allCourses;
|
||||
hasFetched = true;
|
||||
}
|
||||
|
||||
function filterCourses(searchValue: string, _selectedId: string, courses: Course[]) {
|
||||
if (browser) {
|
||||
if (!selectedId) {
|
||||
selectedId = localStorage.getItem('classroomio_filter_course_key') || '0';
|
||||
} else {
|
||||
localStorage.setItem('classroomio_filter_course_key', _selectedId);
|
||||
}
|
||||
}
|
||||
|
||||
filteredExploreCourses = courses.filter((course) => {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (_selectedId === '0') {
|
||||
filteredExploreCourses = filteredExploreCourses.sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
);
|
||||
} else if (_selectedId === '1') {
|
||||
filteredExploreCourses = filteredExploreCourses.sort(
|
||||
(a, b) => b.total_lessons - a.total_lessons
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||
$courseMetaDeta.view = preference;
|
||||
localStorage.setItem('courseView', preference);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const courseView = localStorage.getItem('courseView') as 'grid' | 'list' | null;
|
||||
|
||||
if (courseView) {
|
||||
$courseMetaDeta.view = courseView;
|
||||
}
|
||||
});
|
||||
|
||||
$: filterCourses(searchValue, selectedId, exploreCourseList);
|
||||
|
||||
$: if (browser && $profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="w-full md:max-w-6xl md:mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('explore.heading')}</h1>
|
||||
<div class="flex flex-row-reverse">
|
||||
<div class="filter-container flex items-end justify-start">
|
||||
<Search
|
||||
placeholder={$t('courses.search_placeholder')}
|
||||
bind:value={searchValue}
|
||||
searchClass="mr-2"
|
||||
class=" bg-gray-100 dark:bg-neutral-800"
|
||||
/>
|
||||
<Dropdown
|
||||
class="h-full"
|
||||
size="xl"
|
||||
bind:selectedId
|
||||
items={[
|
||||
{ id: '0', text: $t('courses.course_filter.date_created') },
|
||||
{ id: '1', text: $t('courses.course_filter.lessons') }
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if $courseMetaDeta.view === 'list'}
|
||||
<IconButton onClick={() => setViewPreference('grid')}>
|
||||
<Grid size={24} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => setViewPreference('list')}>
|
||||
<List size={24} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Courses
|
||||
bind:courses={filteredExploreCourses}
|
||||
emptyTitle={$t('explore.empty_heading')}
|
||||
emptyDescription={$t('explore.empty_description')}
|
||||
isExplore={true}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
|
||||
filteredCourses = courses.filter((course) => {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue)) {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE OR REPLACE FUNCTION public.get_explore_courses(org_id_arg uuid, profile_id_arg uuid)
|
||||
RETURNS TABLE(id uuid, org_id uuid, title character varying, slug character varying, description character varying, logo text, banner_image text, cost bigint, currency character varying, is_published boolean, total_lessons bigint, total_students bigint, progress_rate bigint, type "COURSE_TYPE", other_profile_id uuid)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
Return query
|
||||
select course.id, organization.id AS org_id, course.title, course.slug, course.description, course.logo, course.banner_image, course.cost, course.currency, course.is_published, (select COUNT(*) from lesson
|
||||
as l where l.course_id = course.id) AS total_lessons, (select COUNT(*) from groupmember as gm where gm.group_id = course.group_id AND gm.role_id = 3) as total_students, (select COUNT(*) from lesson_completion as lc join lesson as l on l.id = lc.lesson_id where l.course_id = course.id and lc.is_complete = true and lc.profile_id = profile_id_arg) AS progress_rate, course.type as type, (select groupmember.profile_id from groupmember where groupmember.group_id = "group".id and groupmember.profile_id != profile_id_arg
|
||||
limit 1) as other_profile_id
|
||||
from course
|
||||
join "group" on "group".id = course.group_id
|
||||
join organization on organization.id = "group".organization_id
|
||||
where course.status = 'ACTIVE' AND course.is_published = true AND organization.id = org_id_arg AND profile_id_arg NOT IN (SELECT groupmember.profile_id FROM groupmember WHERE groupmember.group_id = course.group_id)
|
||||
ORDER BY course.created_at DESC;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
Reference in New Issue
Block a user