Skip to content

Commit ea91ed7

Browse files
feat: view enrollments
1 parent e542cad commit ea91ed7

File tree

10 files changed

+390
-4
lines changed

10 files changed

+390
-4
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState } from 'react';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import { ActionRow, Button, IconButton } from '@openedx/paragon';
4+
import { MoreVert } from '@openedx/paragon/icons';
5+
import messages from './messages';
6+
import EnrollmentsList from './components/EnrollmentsList';
7+
import EnrollmentStatusModal from './components/EnrollmentStatusModal';
8+
import UnenrollModal from './components/UnenrollModal';
9+
import { Learner } from './types';
10+
11+
const EnrollmentsPage = () => {
12+
const intl = useIntl();
13+
const [isEnrollmentStatusModalOpen, setIsEnrollmentStatusModalOpen] = useState(false);
14+
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
15+
const [selectedLearner, setSelectedLearner] = useState<Learner | null>(null);
16+
17+
const handleMoreButton = () => {
18+
setIsEnrollmentStatusModalOpen(true);
19+
};
20+
21+
const handleUnenroll = (learner: Learner) => {
22+
setIsUnenrollModalOpen(true);
23+
setSelectedLearner(learner);
24+
};
25+
26+
const handleUnenrollModalClose = () => {
27+
setIsUnenrollModalOpen(false);
28+
setSelectedLearner(null);
29+
};
30+
31+
const handleCloseEnrollmentStatusModal = () => {
32+
setIsEnrollmentStatusModalOpen(false);
33+
};
34+
35+
return (
36+
<div className="my-4.5 mx-4">
37+
<div className="d-flex justify-content-between align-items-center">
38+
<h3>{intl.formatMessage(messages.enrollmentsPageTitle)}</h3>
39+
<ActionRow>
40+
<IconButton
41+
alt={intl.formatMessage(messages.checkEnrollmentStatus)}
42+
className="lead"
43+
iconAs={MoreVert}
44+
onClick={handleMoreButton}
45+
/>
46+
<Button variant="outline-primary">+ {intl.formatMessage(messages.addBetaTesters)}</Button>
47+
<Button>+ {intl.formatMessage(messages.enrollLearners)}</Button>
48+
</ActionRow>
49+
</div>
50+
<EnrollmentsList onUnenroll={handleUnenroll} />
51+
<EnrollmentStatusModal isOpen={isEnrollmentStatusModalOpen} onClose={handleCloseEnrollmentStatusModal} />
52+
<UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} />
53+
</div>
54+
);
55+
};
56+
57+
export default EnrollmentsPage;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { useIntl } from '@openedx/frontend-base';
4+
import { Button, FormControl, ModalDialog } from '@openedx/paragon';
5+
import { useEnrollmentByUserId } from '../data/apiHook';
6+
import messages from '../messages';
7+
8+
interface EnrollmentStatusModalProps {
9+
isOpen: boolean,
10+
onClose: () => void,
11+
}
12+
13+
const EnrollmentStatusModal = ({ isOpen, onClose }: EnrollmentStatusModalProps) => {
14+
const intl = useIntl();
15+
const { courseId = '' } = useParams<{ courseId: string }>();
16+
const [learnerIdentifier, setLearnerIdentifier] = useState<string>('');
17+
const { data = { status: '' }, refetch } = useEnrollmentByUserId(courseId, learnerIdentifier);
18+
19+
const handleSearch = async () => {
20+
refetch();
21+
};
22+
23+
return (
24+
<ModalDialog title={intl.formatMessage(messages.checkEnrollmentStatus)} isOpen={isOpen} onClose={onClose} isOverflowVisible={false}>
25+
<ModalDialog.Header><h3 className="text-primary-500">{intl.formatMessage(messages.checkEnrollmentStatus)}</h3></ModalDialog.Header>
26+
<ModalDialog.Body className="py-4">
27+
<p>{intl.formatMessage(messages.addLearnerInstructions)}</p>
28+
<FormControl
29+
placeholder={intl.formatMessage(messages.enrollLearnersPlaceholder)}
30+
value={learnerIdentifier}
31+
onChange={(e) => setLearnerIdentifier(e.target.value)}
32+
/>
33+
<Button
34+
className="mt-3"
35+
onClick={handleSearch}
36+
disabled={!learnerIdentifier.trim()}
37+
>
38+
{intl.formatMessage(messages.checkEnrollmentStatus)}
39+
</Button>
40+
41+
{data.status && learnerIdentifier && (
42+
<p>{intl.formatMessage(messages.statusResponseMessage, { learnerIdentifier, status: data.status })}</p>
43+
)}
44+
</ModalDialog.Body>
45+
<ModalDialog.Footer>
46+
<Button onClick={onClose}>{intl.formatMessage(messages.closeButton)}</Button>
47+
</ModalDialog.Footer>
48+
</ModalDialog>
49+
);
50+
};
51+
52+
export default EnrollmentStatusModal;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useState } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { ActionRow, Button, DataTable, IconButton } from '@openedx/paragon';
4+
import { useIntl } from '@openedx/frontend-base';
5+
import { MoreVert } from '@openedx/paragon/icons';
6+
import messages from '../messages';
7+
import { useEnrollments } from '../data/apiHook';
8+
import { Learner } from '../types';
9+
10+
const ENROLLMENTS_PAGE_SIZE = 25;
11+
12+
const demoEnrollments = [
13+
{
14+
id: '1',
15+
username: 'johndoe',
16+
fullName: 'John Doe',
17+
email: 'johndoe@example.com',
18+
track: 'Audit',
19+
betaTester: true,
20+
actions: <button type="button" className="btn btn-link">Check Enrollment Status</button>,
21+
},
22+
];
23+
24+
interface EnrollmentsListProps {
25+
onUnenroll: (learner: Learner) => void,
26+
}
27+
28+
const EnrollmentsList = ({ onUnenroll }: EnrollmentsListProps) => {
29+
const intl = useIntl();
30+
const { courseId } = useParams();
31+
const [page, setPage] = useState(0);
32+
const { data = { count: 0, results: demoEnrollments }, isLoading } = useEnrollments(courseId ?? '', {
33+
page,
34+
pageSize: ENROLLMENTS_PAGE_SIZE
35+
});
36+
37+
const pageCount = Math.ceil(data.count / ENROLLMENTS_PAGE_SIZE);
38+
39+
const handleFetchData = (state: any) => {
40+
setPage(state.pageIndex);
41+
};
42+
43+
const handleMoreButton = () => {
44+
// Handle more button click
45+
console.log('More button clicked');
46+
};
47+
48+
const tableColumns = [
49+
{ accessor: 'username', Header: intl.formatMessage(messages.username) },
50+
{ accessor: 'fullName', Header: intl.formatMessage(messages.fullName) },
51+
{ accessor: 'email', Header: intl.formatMessage(messages.email) },
52+
{ accessor: 'track', Header: intl.formatMessage(messages.track) },
53+
{ accessor: 'betaTester', Header: intl.formatMessage(messages.betaTester) },
54+
{ accessor: 'actions', Header: intl.formatMessage(messages.actions) },
55+
];
56+
57+
const tableData = data.results.map((learner: Learner) => ({
58+
id: learner.id,
59+
username: learner.username,
60+
fullName: learner.fullName,
61+
email: learner.email,
62+
track: learner.track ?? 'N/A',
63+
betaTester: learner.betaTester ? 'True' : '',
64+
actions: (
65+
<ActionRow className="justify-content-start">
66+
<Button className="pl-0" onClick={() => onUnenroll(learner)} variant="link">
67+
{intl.formatMessage(messages.unenrollButton)}
68+
</Button>
69+
<IconButton
70+
alt={intl.formatMessage(messages.checkEnrollmentStatus)}
71+
className="lead"
72+
iconAs={MoreVert}
73+
onClick={handleMoreButton}
74+
/>
75+
</ActionRow>
76+
),
77+
}));
78+
79+
return (
80+
<DataTable
81+
columns={tableColumns}
82+
data={tableData}
83+
fetchData={handleFetchData}
84+
initialState={{
85+
pageIndex: page,
86+
pageSize: ENROLLMENTS_PAGE_SIZE,
87+
}}
88+
isLoading={isLoading}
89+
isPaginated
90+
itemCount={data.count}
91+
manualFilters
92+
manualPagination
93+
pageSize={ENROLLMENTS_PAGE_SIZE}
94+
pageCount={pageCount}
95+
/>
96+
);
97+
};
98+
99+
export default EnrollmentsList;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Learner } from '../types';
2+
interface UnenrollModalProps {
3+
learner: Learner | null,
4+
isOpen: boolean,
5+
onClose: () => void,
6+
}
7+
8+
const UnenrollModal = ({ learner, isOpen, onClose }: UnenrollModalProps) => {
9+
console.log(learner, isOpen);
10+
11+
if (!isOpen || learner === null) {
12+
onClose();
13+
return null;
14+
}
15+
16+
return <div>Unenroll Modal</div>;
17+
};
18+
19+
export default UnenrollModal;

src/enrollments/data/api.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base';
2+
import { getApiBaseUrl } from '../../data/api';
3+
import { EnrollmentsResponse, EnrollmentStatusResponse } from '../types';
4+
5+
export interface PaginationParams {
6+
page: number,
7+
pageSize: number,
8+
}
9+
10+
export const getEnrollments = async (
11+
courseId: string,
12+
pagination: PaginationParams
13+
): Promise<EnrollmentsResponse> => {
14+
const { data } = await getAuthenticatedHttpClient().get(
15+
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/?page=${pagination.page}&page_size=${pagination.pageSize}`
16+
);
17+
return camelCaseObject(data);
18+
};
19+
20+
export const getEnrollmentStatus = async (
21+
courseId: string,
22+
userIdentifier: string
23+
): Promise<EnrollmentStatusResponse> => {
24+
const { data } = await getAuthenticatedHttpClient().get(
25+
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/?email_or_username=${userIdentifier}`
26+
);
27+
return camelCaseObject(data);
28+
};

src/enrollments/data/apiHook.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { getEnrollments, getEnrollmentStatus, PaginationParams } from './api';
3+
import { enrollmentsQueryKeys } from './queryKeys';
4+
5+
export const useEnrollments = (courseId: string, pagination: PaginationParams) => (
6+
useQuery({
7+
queryKey: enrollmentsQueryKeys.byCoursePaginated(courseId, pagination),
8+
queryFn: () => getEnrollments(courseId, pagination),
9+
})
10+
);
11+
12+
export const useEnrollmentByUserId = (courseId: string, userIdentifier: string) => (
13+
useQuery({
14+
queryKey: enrollmentsQueryKeys.byUserId(courseId, userIdentifier),
15+
queryFn: () => getEnrollmentStatus(courseId, userIdentifier),
16+
enabled: false,
17+
})
18+
);

src/enrollments/data/queryKeys.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { appId } from '../../constants';
2+
import { PaginationParams } from './api';
3+
4+
export const enrollmentsQueryKeys = {
5+
all: [appId, 'enrollments'] as const,
6+
byCourse: (courseId: string) => [...enrollmentsQueryKeys.all, courseId] as const,
7+
byCoursePaginated: (courseId: string, pagination: PaginationParams) => [...enrollmentsQueryKeys.byCourse(courseId), pagination.page] as const,
8+
byUserId: (courseId: string, userIdentifier: string) => [...enrollmentsQueryKeys.byCourse(courseId), 'enrollment', userIdentifier] as const,
9+
};

src/enrollments/messages.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { defineMessages } from '@openedx/frontend-base';
2+
3+
const messages = defineMessages({
4+
enrollmentsPageTitle: {
5+
id: 'instruct.enrollments.page.title',
6+
defaultMessage: 'Enrollment Management',
7+
description: 'Title for the enrollments page',
8+
},
9+
addBetaTesters: {
10+
id: 'instruct.enrollments.addBetaTesters',
11+
defaultMessage: 'Add Beta Testers',
12+
description: 'Button label for adding beta testers',
13+
},
14+
enrollLearners: {
15+
id: 'instruct.enrollments.enrollLearners',
16+
defaultMessage: 'Enroll Learners',
17+
description: 'Button label for enrolling learners',
18+
},
19+
checkEnrollmentStatus: {
20+
id: 'instruct.enrollments.checkEnrollmentStatus',
21+
defaultMessage: 'Check Enrollment Status',
22+
description: 'Check enrollment status modal title and alt for icon button',
23+
},
24+
username: {
25+
id: 'instruct.enrollments.username',
26+
defaultMessage: 'Username',
27+
description: 'Column header for username in enrollments list',
28+
},
29+
fullName: {
30+
id: 'instruct.enrollments.fullName',
31+
defaultMessage: 'Name',
32+
description: 'Column header for full name in enrollments list',
33+
},
34+
email: {
35+
id: 'instruct.enrollments.email',
36+
defaultMessage: 'Email',
37+
description: 'Column header for email in enrollments list',
38+
},
39+
track: {
40+
id: 'instruct.enrollments.track',
41+
defaultMessage: 'Track',
42+
description: 'Column header for track in enrollments list',
43+
},
44+
betaTester: {
45+
id: 'instruct.enrollments.betaTester',
46+
defaultMessage: 'Beta Tester',
47+
description: 'Column header for beta tester status in enrollments list',
48+
},
49+
actions: {
50+
id: 'instruct.enrollments.actions',
51+
defaultMessage: 'Actions',
52+
description: 'Column header for actions in enrollments list',
53+
},
54+
unenrollButton: {
55+
id: 'instruct.enrollments.unenrollButton',
56+
defaultMessage: 'Unenroll',
57+
description: 'Button label for unenrolling a learner',
58+
},
59+
trueLabel: {
60+
id: 'instruct.enrollments.trueLabel',
61+
defaultMessage: 'True',
62+
description: 'Label for true boolean value',
63+
},
64+
addLearnerInstructions: {
65+
id: 'instruct.enrollments.checkEnrollmentStatusModal.addLearnerInstructions',
66+
defaultMessage: 'Learner’s My Open edX email address or username',
67+
description: 'Instructions for enroll learners to the course',
68+
},
69+
enrollLearnersPlaceholder: {
70+
id: 'instruct.enrollments.checkEnrollmentStatusModal.enrollLearnersPlaceholder',
71+
defaultMessage: 'Learner email address or username',
72+
description: 'Placeholder text for enrolling learners textarea',
73+
},
74+
closeButton: {
75+
id: 'instruct.enrollments.checkEnrollmentStatusModal.closeButton',
76+
defaultMessage: 'Close',
77+
description: 'Label for close button in modals',
78+
},
79+
statusResponseMessage: {
80+
id: 'instruct.enrollments.checkEnrollmentStatusModal.statusResponseMessage',
81+
defaultMessage: 'Enrollment status for {learnerIdentifier}: {status}',
82+
description: 'Message displaying the enrollment status for a learner',
83+
}
84+
});
85+
86+
export default messages;

src/enrollments/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export interface EnrollmentsResponse {
2+
count: number,
3+
results: Learner[],
4+
}
5+
6+
export interface EnrollmentStatusResponse {
7+
status: string,
8+
}
9+
10+
export interface Learner {
11+
id: string,
12+
username: string,
13+
fullName: string,
14+
email: string,
15+
track: string,
16+
betaTester: boolean,
17+
};

0 commit comments

Comments
 (0)