Skip to content

Commit

Permalink
feat: List allowances
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Jul 25, 2024
1 parent 8ea9bff commit 83b3434
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const RequestStates = {
};

export const RequestKeys = {
fetchAllowances: 'fetchAllowances',
fetchCourseExams: 'fetchCourseExams',
fetchExamAttempts: 'fetchExamAttempts',
deleteExamAttempt: 'deleteExamAttempt',
Expand Down
122 changes: 107 additions & 15 deletions src/pages/ExamsPage/components/AllowanceList.jsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,136 @@
import PropTypes from 'prop-types';

import {
Button,
Collapsible,
DataTable,
} from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';

import messages from '../messages';
import { useAllowancesData } from '../hooks';

import './AllowanceList.scss';
import AllowanceListActions from './AllowanceListActions';

const AllowanceList = ({ allowances }) => {
const AllowanceList = () => {
const { formatMessage } = useIntl();

const {
allowancesList,
} = useAllowancesData();

const allowancesByUser = allowancesList.reduce((acc, curr) => {
const {
id,
exam_id: examId,
user_id: userId,
extra_time_mins: extraTimeMins,
username,
exam_name: examName,
email,
} = curr;

const allowance = {
id,
examId,
examName,
allowanceType: 'Additional time (minutes)',
extraTimeMins,
};

const user = acc.find(u => u.userId === userId);

if (user) {
user.allowances.push(allowance);

return acc;
}

acc.push({
userId,
username,
email,
allowances: [allowance],
});

return acc;
}, []);

const handleAdd = () => {
console.log('Add'); // eslint-disable-line no-console
};

const handleEdit = (row) => {
console.log('Edit', row); // eslint-disable-line no-console
};

const handleDelete = (row) => {
console.log('Delete', row); // eslint-disable-line no-console
};

return (
<>
<div className="allowances-header">
<h3 data-testid="allowances"> {formatMessage(messages.allowanceDashboardTabTitle)} </h3>
<Button variant="outline-primary" iconBefore={Add} size="sm" className="header-allowance-button">
<h3 data-testid="allowances">{formatMessage(messages.allowanceDashboardTabTitle)}</h3>
<Button variant="outline-primary" iconBefore={Add} size="sm" className="header-allowance-button" onClick={handleAdd}>
{formatMessage(messages.allowanceButton)}
</Button>
</div>
<div className="allowances-body">
{ allowances.length === 0
{!allowancesByUser.length
? (
<>
<div className="allowances-empty">
<h4> {formatMessage(messages.noAllowancesHeader)} </h4>
<p> {formatMessage(messages.noAllowancesBody)} </p>
<Button variant="primary" iconBefore={Add}>
<Button variant="primary" iconBefore={Add} onClick={handleAdd}>
{formatMessage(messages.allowanceButton)}
</Button>
</>
)
: null }
</div>
) : (
<div className="allowances-list">
{allowancesByUser.map(({
userId, username, email, allowances,
}) => (
<Collapsible
defaultOpen
key={userId}
styling="card-lg"
title={<p><strong>{username}</strong> (<a href={`mailto: ${email}`} target="_blank" onClick={e => e.stopPropagation()} rel="noreferrer">{email}</a>)</p>}
>
<DataTable
itemCount={allowances.length}
data={allowances}
columns={[
{
Header: 'Exam Name',
accessor: 'examName',

},
{
Header: 'Allowance Type',
accessor: 'allowanceType',
},
{
Header: 'Allowance Value',
accessor: 'extraTimeMins',
},
]}
additionalColumns={[
{
id: 'actions',
Cell: () => AllowanceListActions({ onEdit: handleEdit, onDelete: handleDelete }),
},
]}
>
<DataTable.Table />
</DataTable>
</Collapsible>
))}
</div>
)}
</div>
</>
);
};

AllowanceList.propTypes = {
allowances: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types
};

export default AllowanceList;
20 changes: 10 additions & 10 deletions src/pages/ExamsPage/components/AllowanceList.scss
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
.allowances-header {
padding: 40px 0 32px;
display: flex;
justify-content: space-between;
gap: 10px;
}

.allowances-body {
display: inline-block;
text-align: center;
width: 100%;
padding-bottom: 50px;
}

.allowances-header {
width: 100%;
padding: 10px;
position: relative;

.header-allowance-button {
position: absolute;
top: 10px;
right: 10px;
}
.allowances-empty {
padding: 100px 20px;
}

41 changes: 41 additions & 0 deletions src/pages/ExamsPage/components/AllowanceListActions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import {
Icon,
IconButtonWithTooltip,
} from '@openedx/paragon';
import { EditOutline, DeleteOutline } from '@openedx/paragon/icons';

import './AllowanceList.scss';

const AllowanceListActions = ({ onEdit, onDelete }) => (
<div className="allowances-actions text-right">
<IconButtonWithTooltip
tooltipPlacement="top"
tooltipContent={<div>Edit allowance</div>}
src={EditOutline}
iconAs={Icon}
alt="Edit"
onClick={onEdit}
variant="primary"
className="mr-2"
size="sm"
/>
<IconButtonWithTooltip
tooltipPlacement="top"
tooltipContent={<div>Delete allowance</div>}
src={DeleteOutline}
iconAs={Icon}
alt="Delete"
onClick={onDelete}
variant="secondary"
size="sm"
/>
</div>
);

AllowanceListActions.propTypes = {
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};

export default AllowanceListActions;
6 changes: 6 additions & 0 deletions src/pages/ExamsPage/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export function getExamsBaseUrl() {
return getConfig().EXAMS_BASE_URL;
}

export async function getAllowances(courseId) {
const url = `${getExamsBaseUrl()}/api/v1/exams/course_id/${courseId}/allowances`;
const response = await getAuthenticatedHttpClient().get(url);
return response.data;
}

export async function getCourseExams(courseId) {
const url = `${getExamsBaseUrl()}/api/v1/exams/course_id/${courseId}/`;
const response = await getAuthenticatedHttpClient().get(url);
Expand Down
26 changes: 26 additions & 0 deletions src/pages/ExamsPage/data/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,31 @@ const slice = createSlice({
...state,
currentExamIndex: getCurrentExamIndex(state.examsList, examId),
}),
setAllowancesList: (state, { payload }) => {
try {
const allowancesList = [...payload];

// Sorting the list by username first and then by exam name.
// Makes it easier to list alphabetically.
allowancesList.sort((a, b) => {
const compare = `${a.username}`.localeCompare(`${b.username}`);

if (compare !== 0) { return compare; }

return `${a.exam_name}`.localeCompare(`${b.exam_name}`);
});

return {
...state,
allowancesList,
};
} catch (e) {
// Something wrong, skip update.
console.error('Error updating allowances list.', payload); // eslint-disable-line no-console

return state;
}
},
},
});

Expand All @@ -100,6 +125,7 @@ export const {
modifyExamAttemptStatus,
setCurrentExam,
setCourseId,
setAllowancesList,
} = slice.actions;

export const {
Expand Down
15 changes: 15 additions & 0 deletions src/pages/ExamsPage/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ export const state = {
exampleValue: (val) => React.useState(val), // eslint-disable-line
};

export const useFetchAllowances = () => {
const makeNetworkRequest = reduxHooks.useMakeNetworkRequest();
const dispatch = useDispatch();
return (courseId) => (
makeNetworkRequest({
requestKey: RequestKeys.fetchAllowances,
promise: api.getAllowances(courseId),
onSuccess: (response) => dispatch(reducer.setAllowancesList(response)),
})
);
};

export const useFetchCourseExams = () => {
const makeNetworkRequest = reduxHooks.useMakeNetworkRequest();
const dispatch = useDispatch();
Expand Down Expand Up @@ -67,9 +79,12 @@ export const useModifyExamAttempt = () => {

export const useInitializeExamsPage = (courseId) => {
const fetchCourseExams = module.useFetchCourseExams();
const fetchAllowances = module.useFetchAllowances();
const dispatch = useDispatch();

React.useEffect(() => {
fetchCourseExams(courseId);
fetchAllowances(courseId);
dispatch(reducer.setCourseId(courseId));
}, []); // eslint-disable-line react-hooks/exhaustive-deps
};
Expand Down
6 changes: 1 addition & 5 deletions src/pages/ExamsPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
useExamsData,
useInitializeExamsPage,
useExamAttemptsData,
useAllowancesData,
} from './hooks';
import AttemptList from './components/AttemptList';
import ExternalReviewDashboard from './components/ExternalReviewDashboard';
Expand All @@ -32,9 +31,6 @@ const ExamsPage = ({ courseId }) => {
const {
attemptsList,
} = useExamAttemptsData();
const {
allowancesList,
} = useAllowancesData();

return (
<Container>
Expand All @@ -53,7 +49,7 @@ const ExamsPage = ({ courseId }) => {
<ExternalReviewDashboard exam={currentExam} />
</Tab>
<Tab eventKey="allowances" title={formatMessage(messages.allowanceDashboardTabTitle)}>
<AllowanceList allowances={allowancesList} />
<AllowanceList />
</Tab>
</Tabs>
</Container>
Expand Down

0 comments on commit 83b3434

Please sign in to comment.