diff --git a/src/Router.tsx b/src/Router.tsx
index fd874c4..136930a 100644
--- a/src/Router.tsx
+++ b/src/Router.tsx
@@ -7,24 +7,23 @@ import { CatalogEditionModalProvider } from './catalogs/useCatalogEditionModal';
const CorporatePartnerPage = lazy(() => import('@src/partner/CorporatePartnerPage'));
const PartnerCatalogsPage = lazy(() => import('@src/catalogs/PartnerCatalogsPage'));
const CoursesPage = lazy(() => import('@src/courses/CoursesPage'));
+const CourseEnrollmentsPage = lazy(() => import('@src/enrollments/CourseEnrollmentsPage'));
const Router = () => (
Loading...}>
-
-
-
-
+
+
+
+
-
-
- Course Details
-
-
- 404 Not Found
-
-
-
+
+
+ 404 Not Found
+
+
+
+
);
diff --git a/src/app/ImageWithSkeleton.tsx b/src/app/ImageWithSkeleton.tsx
index e80f7b9..9b60818 100644
--- a/src/app/ImageWithSkeleton.tsx
+++ b/src/app/ImageWithSkeleton.tsx
@@ -22,7 +22,7 @@ const ImageWithSkeleton: FC = ({
onLoad={() => setImageLoaded(true)}
className={className ?? ''}
style={{
- maxHeight: height, width, display: isImageLoaded ? 'block' : 'none', objectFit: 'cover',
+ maxHeight: height, width, display: isImageLoaded ? 'block' : 'none', objectFit: 'contain',
}}
/>
{!isImageLoaded && }
diff --git a/src/courses/api.ts b/src/courses/api.ts
index bcbfca0..cf56aef 100644
--- a/src/courses/api.ts
+++ b/src/courses/api.ts
@@ -3,12 +3,19 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { CorporateCourse, PaginatedResponse } from '@src/app/types';
-export const getCourses = async (partnerId: string, catalogId: string, pageIndex, pageSize)
-: Promise> => {
+export const getCourses = async (
+ partnerId: string,
+ catalogId: string,
+ pageIndex?: number,
+ pageSize?: number,
+ courseOverview?: string,
+): Promise> => {
try {
const url = new URL(`${getConfig().LMS_BASE_URL}/corporate_access/api/v1/partners/${partnerId}/catalogs/${catalogId}/courses/`);
- url.searchParams.append('page', pageIndex);
- url.searchParams.append('page_size', pageSize);
+ if (courseOverview) { url.searchParams.append('course_overview', courseOverview); }
+ if (pageIndex) { url.searchParams.append('page', String(pageIndex)); }
+ if (pageSize) { url.searchParams.append('page_size', String(pageSize)); }
+
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
} catch (error) {
@@ -27,3 +34,14 @@ export const deleteCourse = async (partnerId: string, catalogId: string, courseI
throw error;
}
};
+
+export const getCourseDetails = async (partnerId: string, catalogId: string, courseId: number)
+: Promise => {
+ try {
+ const { data } = await getAuthenticatedHttpClient().get(`${getConfig().LMS_BASE_URL}/corporate_access/api/v1/partners/${partnerId}/catalogs/${catalogId}/courses/${courseId}/`);
+ return camelCaseObject(data);
+ } catch (error) {
+ logError(error);
+ return null;
+ }
+};
diff --git a/src/courses/components/CoursesList.tsx b/src/courses/components/CoursesList.tsx
index 5502d3a..f21e2d3 100644
--- a/src/courses/components/CoursesList.tsx
+++ b/src/courses/components/CoursesList.tsx
@@ -30,7 +30,9 @@ const CoursesList = ({ partnerId, catalogId }: CoursesListProps) => {
count,
pageCount,
isLoading,
- } = useCatalogCourses(partnerId, catalogId, pageIndex + 1, pageSize);
+ } = useCatalogCourses({
+ partnerId, catalogId, pageIndex: pageIndex + 1, pageSize,
+ });
const deleteCatalogCourse = useDeleteCatalogCourse();
const positions = Array.from({ length: count + 1 || 0 }, (_, i) => i);
diff --git a/src/courses/hooks.test.tsx b/src/courses/hooks.test.tsx
index 34c06e7..18c35f4 100644
--- a/src/courses/hooks.test.tsx
+++ b/src/courses/hooks.test.tsx
@@ -33,7 +33,9 @@ describe('useCatalogCourses', () => {
});
const { result } = renderHook(
- () => useCatalogCourses('p1', 'c1', 1, 10),
+ () => useCatalogCourses({
+ partnerId: 'p1', catalogId: 'c1', pageIndex: 1, pageSize: 10,
+ }),
{ wrapper: createWrapper() },
);
@@ -46,7 +48,9 @@ describe('useCatalogCourses', () => {
it('returns loading state', async () => {
(api.getCourses as jest.Mock).mockImplementation(() => new Promise(() => {}));
- const { result } = renderHook(() => useCatalogCourses('p1', 'c1', 1, 10), {
+ const { result } = renderHook(() => useCatalogCourses({
+ partnerId: 'p1', catalogId: 'c1', pageIndex: 1, pageSize: 10,
+ }), {
wrapper: createWrapper(),
});
@@ -61,7 +65,9 @@ describe('useCatalogCourses', () => {
});
const { result } = renderHook(
- () => useCatalogCourses('p1', 'c1', 1, 10),
+ () => useCatalogCourses({
+ partnerId: 'p1', catalogId: 'c1', pageIndex: 1, pageSize: 10,
+ }),
{ wrapper: createWrapper() },
);
@@ -79,7 +85,9 @@ describe('useCatalogCourses', () => {
});
const { result } = renderHook(
- () => useCatalogCourses('p1', 'c1', 1, 10),
+ () => useCatalogCourses({
+ partnerId: 'p1', catalogId: 'c1', pageIndex: 1, pageSize: 10,
+ }),
{ wrapper: createWrapper() },
);
@@ -91,7 +99,9 @@ describe('useCatalogCourses', () => {
(api.getCourses as jest.Mock).mockRejectedValue(new Error('API error'));
const { result } = renderHook(
- () => useCatalogCourses('p1', 'c1', 1, 10),
+ () => useCatalogCourses({
+ partnerId: 'p1', catalogId: 'c1', pageIndex: 1, pageSize: 10,
+ }),
{ wrapper: createWrapper() },
);
diff --git a/src/courses/hooks.ts b/src/courses/hooks.ts
index bdd7ce0..4549a21 100644
--- a/src/courses/hooks.ts
+++ b/src/courses/hooks.ts
@@ -1,14 +1,31 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { getCourses, deleteCourse } from './api';
+import { CorporateCourse, PaginatedResponse } from '@src/app/types';
+import { getCourses, deleteCourse, getCourseDetails } from './api';
+
+export const useCatalogCourses = ({
+ partnerId, catalogId, pageIndex, pageSize, courseOverview,
+} : {
+ partnerId: string,
+ catalogId: string,
+ pageIndex?: number,
+ pageSize?: number,
+ courseOverview?: string,
+}) => {
+ const queryClient = useQueryClient();
+
+ const courseCached = queryClient
+ .getQueriesData>({ queryKey: ['catalogCourses'] })
+ .flatMap(([, data]) => data?.results ?? [])
+ .filter((course) => course.courseRun.id === courseOverview);
-export const useCatalogCourses = (partnerId: string, catalogId: string, pageIndex, pageSize) => {
const { data, isLoading } = useQuery({
- queryKey: ['catalogCourses', partnerId, catalogId, pageIndex, pageSize],
- queryFn: () => getCourses(partnerId, catalogId, pageIndex, pageSize),
+ queryKey: ['catalogCourses', partnerId, catalogId, pageIndex, pageSize, courseOverview],
+ queryFn: () => getCourses(partnerId, catalogId, pageIndex, pageSize, courseOverview),
+ enabled: !courseCached.length,
});
return {
- courses: data?.results || [], count: data?.count || 0, pageCount: data?.numPages, isLoading,
+ courses: data?.results || courseCached, count: data?.count || 0, pageCount: data?.numPages, isLoading,
};
};
@@ -26,3 +43,25 @@ export const useDeleteCatalogCourse = () => {
});
return mutateAsync;
};
+
+export const useCourseDetails = (
+ { partnerId, catalogId, courseId } : { partnerId: string; catalogId: string; courseId?: number },
+) => {
+ const queryClient = useQueryClient();
+
+ const allCourses = queryClient
+ .getQueriesData>({ queryKey: ['catalogCourses'] })
+ .flatMap(([, data]) => data?.results ?? []);
+ const courseCached: CorporateCourse | undefined = allCourses.find((course) => course.id === Number(courseId));
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['courseDetails', partnerId, catalogId, courseId],
+ queryFn: () => getCourseDetails(partnerId, catalogId, courseId!),
+ enabled: !courseCached && !!courseId,
+ });
+
+ return {
+ courseDetails: courseCached || data,
+ isLoadingCourseDetails: isLoading,
+ };
+};
diff --git a/src/enrollments/CourseEnrollmentsPage.tsx b/src/enrollments/CourseEnrollmentsPage.tsx
new file mode 100644
index 0000000..2877d7c
--- /dev/null
+++ b/src/enrollments/CourseEnrollmentsPage.tsx
@@ -0,0 +1,43 @@
+import { useParams } from 'wouter';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { usePartnerDetails } from '@src/partner/hooks';
+import HeaderDescription from '@src/app/HeaderDescription';
+import { paths } from '@src/constants';
+import { useCatalogCourses, useCourseDetails } from '@src/courses/hooks';
+import AppLayout from '../app/AppLayout';
+import CourseEnrollmentsList from './components/CourseEnrollmentsList';
+import messages from './messages';
+
+const CourseEnrollmentsPage = () => {
+ const intl = useIntl();
+ const { partnerId, catalogId, courseId } = useParams<{ partnerId: string, catalogId: string, courseId: string }>();
+
+ const { courses } = useCatalogCourses({ partnerId, catalogId, courseOverview: courseId });
+
+ const courseCatalogPK = courses.find((course) => course.courseRun.id === courseId)?.id;
+
+ const { partnerDetails } = usePartnerDetails({ partnerId });
+ const { courseDetails } = useCourseDetails({ partnerId, catalogId, courseId: courseCatalogPK });
+
+ return (
+
+ {courseDetails && (
+
+ )}
+
+
+ );
+};
+
+export default CourseEnrollmentsPage;
diff --git a/src/enrollments/api.ts b/src/enrollments/api.ts
new file mode 100644
index 0000000..1a48622
--- /dev/null
+++ b/src/enrollments/api.ts
@@ -0,0 +1,34 @@
+import { camelCaseObject, getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { logError } from '@edx/frontend-platform/logging';
+import { Learner, PaginatedResponse } from '../app/types';
+
+export const getCourseEnrollments = async ({
+ partnerId, catalogId, courseId, pageIndex, pageSize,
+}:{
+ partnerId: string,
+ catalogId: string,
+ courseId: number,
+ pageIndex?: number,
+ pageSize?: number,
+}): Promise> => {
+ try {
+ const url = new URL(`${getConfig().LMS_BASE_URL}/corporate_access/api/v1/partners/${partnerId}/catalogs/${catalogId}/courses/${courseId}/enrollments/`);
+ if (pageIndex) { url.searchParams.append('page', String(pageIndex)); }
+ if (pageSize) { url.searchParams.append('page_size', String(pageSize)); }
+
+ const response = await getAuthenticatedHttpClient().get(url);
+ return camelCaseObject(response.data);
+ } catch (error) {
+ logError(error);
+ return {
+ next: null,
+ previous: null,
+ count: 0,
+ numPages: 0,
+ currentPage: 0,
+ start: 0,
+ results: [],
+ };
+ }
+};
diff --git a/src/enrollments/components/CourseEnrollmentsList.tsx b/src/enrollments/components/CourseEnrollmentsList.tsx
new file mode 100644
index 0000000..01298ad
--- /dev/null
+++ b/src/enrollments/components/CourseEnrollmentsList.tsx
@@ -0,0 +1,71 @@
+import { FC } from 'react';
+import { useParams } from 'wouter';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ DataTable, TextFilter,
+} from '@openedx/paragon';
+
+import { usePagination } from '@src/hooks';
+import TableFooter from '@src/app/TableFooter';
+
+import messages from '../messages';
+import { useCatalogCourseEnrollments } from '../hooks';
+
+const CourseEnrollmentsList: FC<{ courseCatalogPK: number | undefined }> = ({ courseCatalogPK }) => {
+ const intl = useIntl();
+
+ const { partnerId, catalogId } = useParams();
+ const { pageSize, pageIndex, onPaginationChange } = usePagination();
+
+ const {
+ enrollments, count, pageCount, isLoading,
+ } = useCatalogCourseEnrollments({
+ partnerId: partnerId!, catalogId: catalogId!, courseId: courseCatalogPK, pageIndex: pageIndex + 1, pageSize,
+ });
+
+ return (
+ `${value}%`,
+ },
+ ]}
+ >
+
+
+
+
+ );
+};
+
+export default CourseEnrollmentsList;
diff --git a/src/enrollments/hooks.ts b/src/enrollments/hooks.ts
new file mode 100644
index 0000000..d231aa6
--- /dev/null
+++ b/src/enrollments/hooks.ts
@@ -0,0 +1,24 @@
+import { useQuery } from '@tanstack/react-query';
+import { getCourseEnrollments } from './api';
+
+export const useCatalogCourseEnrollments = ({
+ partnerId, catalogId, pageIndex, pageSize, courseId,
+} : {
+ partnerId: string,
+ catalogId: string,
+ pageIndex?: number,
+ pageSize?: number,
+ courseId?: number,
+}) => {
+ const { data, isLoading } = useQuery({
+ queryKey: ['courseLearners', courseId],
+ queryFn: () => getCourseEnrollments({
+ partnerId, catalogId, courseId: courseId!, pageIndex, pageSize,
+ }),
+ enabled: !!courseId,
+ });
+
+ return {
+ enrollments: data?.results || [], count: data?.count || 0, pageCount: data?.numPages, isLoading,
+ };
+};
diff --git a/src/enrollments/messages.js b/src/enrollments/messages.js
new file mode 100644
index 0000000..a528ec5
--- /dev/null
+++ b/src/enrollments/messages.js
@@ -0,0 +1,51 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ titleCorporatePage: {
+ id: 'course.enrollments.page.title',
+ defaultMessage: 'Course Enrollments',
+ description: 'Page title for the course enrollments page',
+ },
+ headerStudentName: {
+ id: 'course.enrollments.table.header.student_name',
+ defaultMessage: 'Student Name',
+ description: 'Header for the student name column',
+ },
+ headerEmail: {
+ id: 'course.enrollments.table.header.email',
+ defaultMessage: 'Email',
+ description: 'Header for the email column',
+ },
+ headerCompletedAssessments: {
+ id: 'course.enrollments.table.header.completed_assessments',
+ defaultMessage: 'Completed Assessments',
+ description: 'Header for the completed assessments column',
+ },
+ headerDueAssessments: {
+ id: 'course.enrollments.table.header.due_assessments',
+ defaultMessage: 'Assessments to be done',
+ description: 'Header for the due assessments column',
+ },
+ headerProgress: {
+ id: 'course.enrollments.table.header.progress',
+ defaultMessage: 'Progress',
+ description: 'Header for the progress column',
+ },
+ infoEnrollments: {
+ id: 'course.enrollments.info.enrollments',
+ defaultMessage: 'Enrollments',
+ description: 'Info for the number of enrollments',
+ },
+ infoCertified: {
+ id: 'course.enrollments.info.certified',
+ defaultMessage: 'Certified students',
+ description: 'Info for the number of certified students',
+ },
+ infoCompletion: {
+ id: 'course.enrollments.info.completion',
+ defaultMessage: 'Completion rate',
+ description: 'Info for the completion rate',
+ },
+});
+
+export default messages;