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
71 changes: 71 additions & 0 deletions src/enrollments/EnrollmentsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 EnrollLearnersModal from './components/EnrollLearnersModal';
import { Learner } from './types';
import AddBetaTestersModal from './components/AddBetaTestersModal';

const EnrollmentsPage = () => {
const intl = useIntl();
const [isEnrollmentStatusModalOpen, setIsEnrollmentStatusModalOpen] = useState(false);
const [isEnrollLearnersModalOpen, setIsEnrollLearnersModalOpen] = useState(false);
const [isAddBetaTestersModalOpen, setIsAddBetaTestersModalOpen] = 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);
};

const handleEnrollLearners = () => {
setIsEnrollLearnersModalOpen(true);
};

const handleAddBetaTesters = () => {
setIsAddBetaTestersModalOpen(true);
};

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" onClick={handleAddBetaTesters}>+ {intl.formatMessage(messages.addBetaTesters)}</Button>
<Button onClick={handleEnrollLearners}>+ {intl.formatMessage(messages.enrollLearners)}</Button>
</ActionRow>
</div>
<EnrollmentsList onUnenroll={handleUnenroll} />
<EnrollmentStatusModal isOpen={isEnrollmentStatusModalOpen} onClose={handleCloseEnrollmentStatusModal} />
<UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} />
<EnrollLearnersModal isOpen={isEnrollLearnersModalOpen} onClose={() => setIsEnrollLearnersModalOpen(false)} onSuccess={() => {}} />
<AddBetaTestersModal isOpen={isAddBetaTestersModalOpen} onClose={() => setIsAddBetaTestersModalOpen(false)} onSuccess={() => {}} />
</div>
);
};

export default EnrollmentsPage;
40 changes: 40 additions & 0 deletions src/enrollments/components/AddBetaTestersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'react-router-dom';
import { useAddBetaTesters } from '../data/apiHook';
import AddModal from './AddModal';
import { useIntl } from '@openedx/frontend-base';
import messages from '../messages';

export interface AddBetaTestersModalProps {
isOpen: boolean,
onClose: () => void,
onSuccess: () => void,
}

const AddBetaTestersModal = ({ isOpen, onClose, onSuccess }: AddBetaTestersModalProps) => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { mutate: addBetaTesters } = useAddBetaTesters(courseId);

const handleEnroll = (emailList: string[]) => {
addBetaTesters(emailList, {
onSuccess: () => {
onSuccess();
onClose();
},
onError: (error) => {
console.error(error);
}
});
};
return (
<AddModal
instructions={intl.formatMessage(messages.addBetaTestersInstructions)}
isOpen={isOpen}
title={intl.formatMessage(messages.addBetaTesters)}
onClose={onClose}
onSave={handleEnroll}
/>
);
};

export default AddBetaTestersModal;
66 changes: 66 additions & 0 deletions src/enrollments/components/AddModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState } from 'react';
import { useIntl } from '@openedx/frontend-base';
import { Button, FormControl, ModalDialog } from '@openedx/paragon';
import messages from '../messages';
import { FormCheckbox, FormCheckboxSet } from '@openedx/paragon/dist/Form';

export interface AddModalProps {
instructions: string,
isOpen: boolean,
title: string,
onClose: () => void,
onSave: (emailList: string[]) => void,
}

const AddModal = ({
instructions,
isOpen,
title,
onClose,
onSave }: AddModalProps) => {
const intl = useIntl();
const [emails, setEmails] = useState('');

const handleSave = () => {
const emailList = emails.split(',').map(email => email.trim()).filter(email => email);
onSave(emailList);
};

return (
<ModalDialog isOpen={isOpen} onClose={onClose} isOverflowVisible={false} title={title}>
<ModalDialog.Header className="border-light-700 border-bottom">
<h3 className="text-primary-500">{title}</h3>
</ModalDialog.Header>
<ModalDialog.Body className="py-4">
{/* TABS will be added as a follow up */}
{/* <Tabs id={`${title.replace(/\s+/g, '-')}-tabs`} className="mt-0 mb-2" onSelect={() => {}}>
<Tab key={`${title.replace(/\s+/g, '-')}`} eventKey={`${title.replace(/\s+/g, '-')}`} title={title}> */}
<p className="text-gray-700 x-small mb-2">{instructions}</p>
<FormControl
as="textarea"
rows={4}
placeholder={intl.formatMessage(messages.userIdentifierPlaceholder)}
onChange={(e) => setEmails(e.target.value)}
/>
<FormCheckboxSet isInline className="mt-3 text-primary-500">
<FormCheckbox controlClassName="border-primary-500">{intl.formatMessage(messages.autoEnrollCheckbox)}</FormCheckbox>
<FormCheckbox controlClassName="border-primary-500" className="ml-4">{intl.formatMessage(messages.notifyUsersCheckbox)}</FormCheckbox>
</FormCheckboxSet>
{/* </Tab>
<Tab key="upload-csv" eventKey="upload-csv" title={intl.formatMessage(messages.uploadCSV)}>
</Tab>
</Tabs> */}
</ModalDialog.Body>
<ModalDialog.Footer className="border-light-700 border-top">
<Button variant="tertiary" onClick={onClose}>
{intl.formatMessage(messages.cancelButton)}
</Button>
<Button className="ml-2" variant="primary" onClick={handleSave} disabled={emails.trim().length === 0}>
{intl.formatMessage(messages.saveButton)}
</Button>
</ModalDialog.Footer>
</ModalDialog>
);
};

export default AddModal;
40 changes: 40 additions & 0 deletions src/enrollments/components/EnrollLearnersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'react-router-dom';
import { useEnrollLearners } from '../data/apiHook';
import AddModal from './AddModal';
import { useIntl } from '@openedx/frontend-base';
import messages from '../messages';

export interface EnrollLearnersModalProps {
isOpen: boolean,
onClose: () => void,
onSuccess: () => void,
}

const EnrollLearnersModal = ({ isOpen, onClose, onSuccess }: EnrollLearnersModalProps) => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { mutate: enrollLearners } = useEnrollLearners(courseId);

const handleEnroll = (emailList: string[]) => {
enrollLearners(emailList, {
onSuccess: () => {
onSuccess();
onClose();
},
onError: (error) => {
console.error(error);
}
});
};
return (
<AddModal
instructions={intl.formatMessage(messages.enrollLearnerInstructions)}
isOpen={isOpen}
title={intl.formatMessage(messages.enrollLearners)}
onClose={onClose}
onSave={handleEnroll}
/>
);
};

export default EnrollLearnersModal;
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.enrollmentStatusPlaceholder)}
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;
Loading