Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/enrollments/EnrollmentsPage.tsx
Original file line number Diff line number Diff line change
@@ -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<Learner | null>(null);

const handleMoreButton = () => {
setIsEnrollmentStatusModalOpen(true);
};

const handleUnenroll = (learner: Learner) => {
setIsUnenrollModalOpen(true);
setSelectedLearner(learner);
};

const handleUnenrollModalClose = () => {
setIsUnenrollModalOpen(false);
setSelectedLearner(null);
};

const handleCloseEnrollmentStatusModal = () => {
setIsEnrollmentStatusModalOpen(false);
};

return (
<div className="my-4.5 mx-4">
<div className="d-flex justify-content-between align-items-center">
<h3>{intl.formatMessage(messages.enrollmentsPageTitle)}</h3>
<ActionRow>
<IconButton
alt={intl.formatMessage(messages.checkEnrollmentStatus)}
className="lead"
iconAs={MoreVert}
onClick={handleMoreButton}
/>
<Button variant="outline-primary">+ {intl.formatMessage(messages.addBetaTesters)}</Button>
<Button>+ {intl.formatMessage(messages.enrollLearners)}</Button>
</ActionRow>
</div>
<EnrollmentsList onUnenroll={handleUnenroll} />
<EnrollmentStatusModal isOpen={isEnrollmentStatusModalOpen} onClose={handleCloseEnrollmentStatusModal} />
<UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} />
</div>
);
};

export default EnrollmentsPage;
52 changes: 52 additions & 0 deletions src/enrollments/components/EnrollmentStatusModal.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
const { data = { status: '' }, refetch } = useEnrollmentByUserId(courseId, learnerIdentifier);

const handleSearch = async () => {
refetch();
};

return (
<ModalDialog title={intl.formatMessage(messages.checkEnrollmentStatus)} isOpen={isOpen} onClose={onClose} isOverflowVisible={false}>
<ModalDialog.Header><h3 className="text-primary-500">{intl.formatMessage(messages.checkEnrollmentStatus)}</h3></ModalDialog.Header>
<ModalDialog.Body className="py-4">
<p>{intl.formatMessage(messages.addLearnerInstructions)}</p>
<FormControl
placeholder={intl.formatMessage(messages.enrollLearnersPlaceholder)}
value={learnerIdentifier}
onChange={(e) => setLearnerIdentifier(e.target.value)}
/>
<Button
className="mt-3"
onClick={handleSearch}
disabled={!learnerIdentifier.trim()}
>
{intl.formatMessage(messages.checkEnrollmentStatus)}
</Button>

{data.status && learnerIdentifier && (
<p>{intl.formatMessage(messages.statusResponseMessage, { learnerIdentifier, status: data.status })}</p>
)}
</ModalDialog.Body>
<ModalDialog.Footer>
<Button onClick={onClose}>{intl.formatMessage(messages.closeButton)}</Button>
</ModalDialog.Footer>
</ModalDialog>
);
};

export default EnrollmentStatusModal;
99 changes: 99 additions & 0 deletions src/enrollments/components/EnrollmentsList.tsx
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
track: 'Audit',
betaTester: true,
actions: <button type="button" className="btn btn-link">Check Enrollment Status</button>,
},
];

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: (
<ActionRow className="justify-content-start">
<Button className="pl-0" onClick={() => onUnenroll(learner)} variant="link">
{intl.formatMessage(messages.unenrollButton)}
</Button>
<IconButton
alt={intl.formatMessage(messages.checkEnrollmentStatus)}
className="lead"
iconAs={MoreVert}
onClick={handleMoreButton}
/>
</ActionRow>
),
}));

return (
<DataTable
columns={tableColumns}
data={tableData}
fetchData={handleFetchData}
initialState={{
pageIndex: page,
pageSize: ENROLLMENTS_PAGE_SIZE,
}}
isLoading={isLoading}
isPaginated
itemCount={data.count}
manualFilters
manualPagination
pageSize={ENROLLMENTS_PAGE_SIZE}
pageCount={pageCount}
/>
);
};

export default EnrollmentsList;
19 changes: 19 additions & 0 deletions src/enrollments/components/UnenrollModal.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Unenroll Modal</div>;
};

export default UnenrollModal;
28 changes: 28 additions & 0 deletions src/enrollments/data/api.ts
Original file line number Diff line number Diff line change
@@ -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<EnrollmentsResponse> => {
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<EnrollmentStatusResponse> => {
const { data } = await getAuthenticatedHttpClient().get(
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/?email_or_username=${userIdentifier}`
);
return camelCaseObject(data);
};
18 changes: 18 additions & 0 deletions src/enrollments/data/apiHook.ts
Original file line number Diff line number Diff line change
@@ -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,
})
);
9 changes: 9 additions & 0 deletions src/enrollments/data/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -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,
};
86 changes: 86 additions & 0 deletions src/enrollments/messages.ts
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 17 additions & 0 deletions src/enrollments/types.ts
Original file line number Diff line number Diff line change
@@ -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,
};
Loading