diff --git a/src/enrollments/EnrollmentsPage.tsx b/src/enrollments/EnrollmentsPage.tsx new file mode 100644 index 0000000..e5c5d4c --- /dev/null +++ b/src/enrollments/EnrollmentsPage.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { useIntl } from '@openedx/frontend-base'; +import { ActionRow, Button, IconButton } from '@openedx/paragon'; +import { MoreVert } from '@openedx/paragon/icons'; +import messages from './messages'; +import EnrollmentsList from './components/EnrollmentsList'; +import EnrollmentStatusModal from './components/EnrollmentStatusModal'; +import UnenrollModal from './components/UnenrollModal'; +import { Learner } from './types'; + +const EnrollmentsPage = () => { + const intl = useIntl(); + const [isEnrollmentStatusModalOpen, setIsEnrollmentStatusModalOpen] = useState(false); + const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); + const [selectedLearner, setSelectedLearner] = useState(null); + + const handleMoreButton = () => { + setIsEnrollmentStatusModalOpen(true); + }; + + const handleUnenroll = (learner: Learner) => { + setIsUnenrollModalOpen(true); + setSelectedLearner(learner); + }; + + const handleUnenrollModalClose = () => { + setIsUnenrollModalOpen(false); + setSelectedLearner(null); + }; + + const handleCloseEnrollmentStatusModal = () => { + setIsEnrollmentStatusModalOpen(false); + }; + + return ( +
+
+

{intl.formatMessage(messages.enrollmentsPageTitle)}

+ + + + + +
+ + + +
+ ); +}; + +export default EnrollmentsPage; diff --git a/src/enrollments/components/EnrollmentStatusModal.tsx b/src/enrollments/components/EnrollmentStatusModal.tsx new file mode 100644 index 0000000..c195742 --- /dev/null +++ b/src/enrollments/components/EnrollmentStatusModal.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useIntl } from '@openedx/frontend-base'; +import { Button, FormControl, ModalDialog } from '@openedx/paragon'; +import { useEnrollmentByUserId } from '../data/apiHook'; +import messages from '../messages'; + +interface EnrollmentStatusModalProps { + isOpen: boolean, + onClose: () => void, +} + +const EnrollmentStatusModal = ({ isOpen, onClose }: EnrollmentStatusModalProps) => { + const intl = useIntl(); + const { courseId = '' } = useParams<{ courseId: string }>(); + const [learnerIdentifier, setLearnerIdentifier] = useState(''); + const { data = { status: '' }, refetch } = useEnrollmentByUserId(courseId, learnerIdentifier); + + const handleSearch = async () => { + refetch(); + }; + + return ( + +

{intl.formatMessage(messages.checkEnrollmentStatus)}

+ +

{intl.formatMessage(messages.addLearnerInstructions)}

+ setLearnerIdentifier(e.target.value)} + /> + + + {data.status && learnerIdentifier && ( +

{intl.formatMessage(messages.statusResponseMessage, { learnerIdentifier, status: data.status })}

+ )} +
+ + + +
+ ); +}; + +export default EnrollmentStatusModal; diff --git a/src/enrollments/components/EnrollmentsList.tsx b/src/enrollments/components/EnrollmentsList.tsx new file mode 100644 index 0000000..c81cd99 --- /dev/null +++ b/src/enrollments/components/EnrollmentsList.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { ActionRow, Button, DataTable, IconButton } from '@openedx/paragon'; +import { useIntl } from '@openedx/frontend-base'; +import { MoreVert } from '@openedx/paragon/icons'; +import messages from '../messages'; +import { useEnrollments } from '../data/apiHook'; +import { Learner } from '../types'; + +const ENROLLMENTS_PAGE_SIZE = 25; + +const demoEnrollments = [ + { + id: '1', + username: 'johndoe', + fullName: 'John Doe', + email: 'johndoe@example.com', + track: 'Audit', + betaTester: true, + actions: , + }, +]; + +interface EnrollmentsListProps { + onUnenroll: (learner: Learner) => void, +} + +const EnrollmentsList = ({ onUnenroll }: EnrollmentsListProps) => { + const intl = useIntl(); + const { courseId } = useParams(); + const [page, setPage] = useState(0); + const { data = { count: 0, results: demoEnrollments }, isLoading } = useEnrollments(courseId ?? '', { + page, + pageSize: ENROLLMENTS_PAGE_SIZE + }); + + const pageCount = Math.ceil(data.count / ENROLLMENTS_PAGE_SIZE); + + const handleFetchData = (state: any) => { + setPage(state.pageIndex); + }; + + const handleMoreButton = () => { + // Handle more button click + console.log('More button clicked'); + }; + + const tableColumns = [ + { accessor: 'username', Header: intl.formatMessage(messages.username) }, + { accessor: 'fullName', Header: intl.formatMessage(messages.fullName) }, + { accessor: 'email', Header: intl.formatMessage(messages.email) }, + { accessor: 'track', Header: intl.formatMessage(messages.track) }, + { accessor: 'betaTester', Header: intl.formatMessage(messages.betaTester) }, + { accessor: 'actions', Header: intl.formatMessage(messages.actions) }, + ]; + + const tableData = data.results.map((learner: Learner) => ({ + id: learner.id, + username: learner.username, + fullName: learner.fullName, + email: learner.email, + track: learner.track ?? 'N/A', + betaTester: learner.betaTester ? 'True' : '', + actions: ( + + + + + ), + })); + + return ( + + ); +}; + +export default EnrollmentsList; diff --git a/src/enrollments/components/UnenrollModal.tsx b/src/enrollments/components/UnenrollModal.tsx new file mode 100644 index 0000000..029a58d --- /dev/null +++ b/src/enrollments/components/UnenrollModal.tsx @@ -0,0 +1,19 @@ +import { Learner } from '../types'; +interface UnenrollModalProps { + learner: Learner | null, + isOpen: boolean, + onClose: () => void, +} + +const UnenrollModal = ({ learner, isOpen, onClose }: UnenrollModalProps) => { + console.log(learner, isOpen); + + if (!isOpen || learner === null) { + onClose(); + return null; + } + + return
Unenroll Modal
; +}; + +export default UnenrollModal; diff --git a/src/enrollments/data/api.ts b/src/enrollments/data/api.ts new file mode 100644 index 0000000..48a027c --- /dev/null +++ b/src/enrollments/data/api.ts @@ -0,0 +1,28 @@ +import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base'; +import { getApiBaseUrl } from '../../data/api'; +import { EnrollmentsResponse, EnrollmentStatusResponse } from '../types'; + +export interface PaginationParams { + page: number, + pageSize: number, +} + +export const getEnrollments = async ( + courseId: string, + pagination: PaginationParams +): Promise => { + const { data } = await getAuthenticatedHttpClient().get( + `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/?page=${pagination.page}&page_size=${pagination.pageSize}` + ); + return camelCaseObject(data); +}; + +export const getEnrollmentStatus = async ( + courseId: string, + userIdentifier: string +): Promise => { + const { data } = await getAuthenticatedHttpClient().get( + `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/?email_or_username=${userIdentifier}` + ); + return camelCaseObject(data); +}; diff --git a/src/enrollments/data/apiHook.ts b/src/enrollments/data/apiHook.ts new file mode 100644 index 0000000..f9a00ff --- /dev/null +++ b/src/enrollments/data/apiHook.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { getEnrollments, getEnrollmentStatus, PaginationParams } from './api'; +import { enrollmentsQueryKeys } from './queryKeys'; + +export const useEnrollments = (courseId: string, pagination: PaginationParams) => ( + useQuery({ + queryKey: enrollmentsQueryKeys.byCoursePaginated(courseId, pagination), + queryFn: () => getEnrollments(courseId, pagination), + }) +); + +export const useEnrollmentByUserId = (courseId: string, userIdentifier: string) => ( + useQuery({ + queryKey: enrollmentsQueryKeys.byUserId(courseId, userIdentifier), + queryFn: () => getEnrollmentStatus(courseId, userIdentifier), + enabled: false, + }) +); diff --git a/src/enrollments/data/queryKeys.ts b/src/enrollments/data/queryKeys.ts new file mode 100644 index 0000000..2568926 --- /dev/null +++ b/src/enrollments/data/queryKeys.ts @@ -0,0 +1,9 @@ +import { appId } from '../../constants'; +import { PaginationParams } from './api'; + +export const enrollmentsQueryKeys = { + all: [appId, 'enrollments'] as const, + byCourse: (courseId: string) => [...enrollmentsQueryKeys.all, courseId] as const, + byCoursePaginated: (courseId: string, pagination: PaginationParams) => [...enrollmentsQueryKeys.byCourse(courseId), pagination.page] as const, + byUserId: (courseId: string, userIdentifier: string) => [...enrollmentsQueryKeys.byCourse(courseId), 'enrollment', userIdentifier] as const, +}; diff --git a/src/enrollments/messages.ts b/src/enrollments/messages.ts new file mode 100644 index 0000000..b1baa1e --- /dev/null +++ b/src/enrollments/messages.ts @@ -0,0 +1,86 @@ +import { defineMessages } from '@openedx/frontend-base'; + +const messages = defineMessages({ + enrollmentsPageTitle: { + id: 'instruct.enrollments.page.title', + defaultMessage: 'Enrollment Management', + description: 'Title for the enrollments page', + }, + addBetaTesters: { + id: 'instruct.enrollments.addBetaTesters', + defaultMessage: 'Add Beta Testers', + description: 'Button label for adding beta testers', + }, + enrollLearners: { + id: 'instruct.enrollments.enrollLearners', + defaultMessage: 'Enroll Learners', + description: 'Button label for enrolling learners', + }, + checkEnrollmentStatus: { + id: 'instruct.enrollments.checkEnrollmentStatus', + defaultMessage: 'Check Enrollment Status', + description: 'Check enrollment status modal title and alt for icon button', + }, + username: { + id: 'instruct.enrollments.username', + defaultMessage: 'Username', + description: 'Column header for username in enrollments list', + }, + fullName: { + id: 'instruct.enrollments.fullName', + defaultMessage: 'Name', + description: 'Column header for full name in enrollments list', + }, + email: { + id: 'instruct.enrollments.email', + defaultMessage: 'Email', + description: 'Column header for email in enrollments list', + }, + track: { + id: 'instruct.enrollments.track', + defaultMessage: 'Track', + description: 'Column header for track in enrollments list', + }, + betaTester: { + id: 'instruct.enrollments.betaTester', + defaultMessage: 'Beta Tester', + description: 'Column header for beta tester status in enrollments list', + }, + actions: { + id: 'instruct.enrollments.actions', + defaultMessage: 'Actions', + description: 'Column header for actions in enrollments list', + }, + unenrollButton: { + id: 'instruct.enrollments.unenrollButton', + defaultMessage: 'Unenroll', + description: 'Button label for unenrolling a learner', + }, + trueLabel: { + id: 'instruct.enrollments.trueLabel', + defaultMessage: 'True', + description: 'Label for true boolean value', + }, + addLearnerInstructions: { + id: 'instruct.enrollments.checkEnrollmentStatusModal.addLearnerInstructions', + defaultMessage: 'Learner’s My Open edX email address or username', + description: 'Instructions for enroll learners to the course', + }, + enrollLearnersPlaceholder: { + id: 'instruct.enrollments.checkEnrollmentStatusModal.enrollLearnersPlaceholder', + defaultMessage: 'Learner email address or username', + description: 'Placeholder text for enrolling learners textarea', + }, + closeButton: { + id: 'instruct.enrollments.checkEnrollmentStatusModal.closeButton', + defaultMessage: 'Close', + description: 'Label for close button in modals', + }, + statusResponseMessage: { + id: 'instruct.enrollments.checkEnrollmentStatusModal.statusResponseMessage', + defaultMessage: 'Enrollment status for {learnerIdentifier}: {status}', + description: 'Message displaying the enrollment status for a learner', + } +}); + +export default messages; diff --git a/src/enrollments/types.ts b/src/enrollments/types.ts new file mode 100644 index 0000000..e073492 --- /dev/null +++ b/src/enrollments/types.ts @@ -0,0 +1,17 @@ +export interface EnrollmentsResponse { + count: number, + results: Learner[], +} + +export interface EnrollmentStatusResponse { + status: string, +} + +export interface Learner { + id: string, + username: string, + fullName: string, + email: string, + track: string, + betaTester: boolean, +}; diff --git a/src/routes.tsx b/src/routes.tsx index 96c1ac9..f1ab029 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,5 +1,6 @@ import CohortsPage from './cohorts/CohortsPage'; import CourseInfoPage from './courseInfo/CourseInfoPage'; +import EnrollmentsPage from './enrollments/EnrollmentsPage'; import Main from './Main'; const routes = [ @@ -15,10 +16,10 @@ const routes = [ path: 'course_info', element: }, - // { - // path: 'membership', - // element: - // }, + { + path: 'enrollments', + element: + }, { path: 'cohorts', element: