diff --git a/src/features/Dashboard/DashboardPage/index.scss b/src/features/Dashboard/DashboardPage/index.scss
index 6f9ccdab..a51d46bd 100644
--- a/src/features/Dashboard/DashboardPage/index.scss
+++ b/src/features/Dashboard/DashboardPage/index.scss
@@ -14,3 +14,7 @@
box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
border-radius: 0.375rem;
}
+
+.schedule-section {
+ margin-bottom: 2rem;
+}
diff --git a/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx b/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx
index c98517b1..550a14c5 100644
--- a/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx
+++ b/src/features/Dashboard/InstructorAssignSection/_test_/index.test.jsx
@@ -10,7 +10,7 @@ jest.mock('@edx/frontend-platform/logging', () => ({
describe('Instructor Assign component', () => {
const mockStore = {
dashboard: {
- classes: {
+ classesNoInstructors: {
data: [
{
classId: 'ccx-v1:demo+demo1+2020+ccx1',
diff --git a/src/features/Dashboard/InstructorAssignSection/index.jsx b/src/features/Dashboard/InstructorAssignSection/index.jsx
index 8b4c9deb..d7d4cba4 100644
--- a/src/features/Dashboard/InstructorAssignSection/index.jsx
+++ b/src/features/Dashboard/InstructorAssignSection/index.jsx
@@ -5,20 +5,20 @@ import { Row, Col } from '@edx/paragon';
import ClassCard from 'features/Dashboard/InstructorAssignSection/ClassCard';
import { Button } from 'react-paragon-topaz';
-import { fetchClassesData } from 'features/Dashboard/data';
+import { fetchClassesNoInstructorsData } from 'features/Dashboard/data';
import 'features/Dashboard/InstructorAssignSection/index.scss';
const InstructorAssignSection = () => {
const dispatch = useDispatch();
const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
- const classesData = useSelector((state) => state.dashboard.classes.data);
+ const classesData = useSelector((state) => state.dashboard.classesNoInstructors.data);
const [classCards, setClassCards] = useState([]);
const numberOfClasses = 2;
useEffect(() => {
if (Object.keys(selectedInstitution).length > 0) {
- dispatch(fetchClassesData(selectedInstitution?.id));
+ dispatch(fetchClassesNoInstructorsData(selectedInstitution?.id));
}
}, [selectedInstitution, dispatch]);
diff --git a/src/features/Dashboard/WeeklySchedule/_test_/index.test.jsx b/src/features/Dashboard/WeeklySchedule/_test_/index.test.jsx
new file mode 100644
index 00000000..23f553ca
--- /dev/null
+++ b/src/features/Dashboard/WeeklySchedule/_test_/index.test.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import WeeklySchedule from 'features/Dashboard/WeeklySchedule';
+import '@testing-library/jest-dom/extend-expect';
+import { renderWithProviders } from 'test-utils';
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+}));
+
+describe('WeeklySchedule component', () => {
+ const mockStore = {
+ dashboard: {
+ classes: {
+ data: [
+ {
+ classId: 'ccx-v1:demo+demo1+2020+ccx1',
+ className: 'ccx 1',
+ masterCourseName: 'Demo Course 1',
+ instructors: [
+ 'instructor1',
+ ],
+ numberOfStudents: 0,
+ numberOfPendingStudents: 0,
+ maxStudents: 20,
+ startDate: '2024-01-23T21:50:51Z',
+ endDate: null,
+ },
+ {
+ classId: 'ccx-v1:demo+demo1+2020+ccx2',
+ className: 'ccx 2',
+ masterCourseName: 'Demo Course 1',
+ instructors: [
+ 'instructor1',
+ ],
+ numberOfStudents: 0,
+ numberOfPendingStudents: 0,
+ maxStudents: 20,
+ startDate: '2023-10-02T00:58:43Z',
+ endDate: null,
+ },
+ ],
+ },
+ },
+ };
+ const component = renderWithProviders(
+
,
+ { preloadedState: mockStore },
+ );
+
+ test('renders components', () => {
+ const { getByText } = component;
+
+ expect(getByText('Class schedule')).toBeInTheDocument();
+ expect(getByText('ccx 1')).toBeInTheDocument();
+ expect(getByText('Jan 23, 2024')).toBeInTheDocument();
+ expect(getByText('ccx 2')).toBeInTheDocument();
+ expect(getByText('Oct 2, 2023')).toBeInTheDocument();
+ });
+});
diff --git a/src/features/Dashboard/WeeklySchedule/index.jsx b/src/features/Dashboard/WeeklySchedule/index.jsx
new file mode 100644
index 00000000..c0dbdffc
--- /dev/null
+++ b/src/features/Dashboard/WeeklySchedule/index.jsx
@@ -0,0 +1,79 @@
+import React, { useEffect, useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+
+import { DateRange } from 'react-date-range';
+import {
+ startOfWeek,
+ endOfWeek,
+ format,
+} from 'date-fns';
+
+import { fetchClassesData } from 'features/Dashboard/data';
+
+import 'features/Dashboard/WeeklySchedule/index.scss';
+
+const WeeklySchedule = () => {
+ const dispatch = useDispatch();
+ const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
+ const classesData = useSelector((state) => state.dashboard.classes.data);
+ const [classList, setClassList] = useState([]);
+ const startWeek = startOfWeek(new Date());
+ const endWeek = endOfWeek(new Date());
+ const [stateDate, setStateDate] = useState([
+ {
+ startDate: startWeek,
+ endDate: endWeek,
+ key: 'selection',
+ },
+ ]);
+ const numberOfClasses = 3;
+
+ useEffect(() => {
+ if (Object.keys(selectedInstitution).length > 0) {
+ dispatch(fetchClassesData(selectedInstitution?.id));
+ }
+ }, [selectedInstitution, dispatch]);
+
+ useEffect(() => {
+ // Display only the first 'NumberOfClasses' on the homepage.
+ if (classesData.length > numberOfClasses) {
+ setClassList(classesData.slice(0, numberOfClasses));
+ } else {
+ setClassList(classesData);
+ }
+ }, [classesData]);
+
+ return (
+ <>
+
+
Class schedule
+
+
+
+ {classList.map(classInfo => {
+ const date = format(new Date(classInfo?.startDate), 'PP');
+ return (
+
+
+
{classInfo?.className}
+
+ {date}
+
+
+
+ );
+ })}
+
+
setStateDate([item.selection])}
+ moveRangeOnFirstSelection={false}
+ ranges={stateDate}
+ rangeColors={['#e4faff']}
+ />
+
+ >
+ );
+};
+
+export default WeeklySchedule;
diff --git a/src/features/Dashboard/WeeklySchedule/index.scss b/src/features/Dashboard/WeeklySchedule/index.scss
new file mode 100644
index 00000000..b159f9ff
--- /dev/null
+++ b/src/features/Dashboard/WeeklySchedule/index.scss
@@ -0,0 +1,82 @@
+@import "assets/colors.scss";
+
+.header-schedule {
+ background-color: $primary;
+ padding: 1rem;
+ border-top-right-radius: 0.375rem;
+ border-top-left-radius: 0.375rem;
+
+ h3 {
+ color: $color-white;
+ margin: 0;
+ }
+}
+
+.content-schedule {
+ box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
+ border-radius: 0.375rem;
+
+ .rdrDay:not(.rdrDayPassive) .rdrInRange ~ .rdrDayNumber span,
+ .rdrDay:not(.rdrDayPassive) .rdrStartEdge ~ .rdrDayNumber span,
+ .rdrDay:not(.rdrDayPassive) .rdrEndEdge ~ .rdrDayNumber span,
+ .rdrDay:not(.rdrDayPassive) .rdrSelected ~ .rdrDayNumber span {
+ color: $primary;
+ }
+
+ .rdrStartEdge {
+ border-top: 1px solid $primary;
+ border-left: 1px solid $primary;
+ border-bottom: 1px solid $primary;
+ }
+
+ .rdrInRange {
+ border-top: 1px solid $primary;
+ border-bottom: 1px solid $primary;
+ }
+
+ .rdrEndEdge {
+ border-top: 1px solid $primary;
+ border-right : 1px solid $primary;
+ border-bottom: 1px solid $primary;
+ }
+
+ .rdrCalendarWrapper {
+ border-radius: 0.375rem;
+ }
+}
+
+.container-class-schedule {
+ width: 100%;
+ background: $color-white;
+ padding: 0.8rem;
+ border-radius: 0.375rem;
+ border-right: 1px solid $gray-20;
+
+ .class-schedule {
+ margin-top: 0.5rem;
+ border-bottom: 1px solid $gray-20;
+
+ .class-name {
+ margin: 0;
+ font-weight: 600;
+ color: $primary;
+ }
+
+ .class-descr {
+ margin: 0;
+ font-size: 12px;
+ }
+
+ .fa-calendar-day {
+ margin-right: 8px;
+ }
+ }
+
+ .class-text {
+ border-left: 3px solid $primary;
+ padding-left: 1rem;
+ padding-left: 1rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ }
+}
diff --git a/src/features/Dashboard/data/_test_/redux.test.jsx b/src/features/Dashboard/data/_test_/redux.test.jsx
index ac4ce56f..419d8f06 100644
--- a/src/features/Dashboard/data/_test_/redux.test.jsx
+++ b/src/features/Dashboard/data/_test_/redux.test.jsx
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing';
-import { fetchLicensesData, fetchClassesData } from 'features/Dashboard/data';
+import { fetchLicensesData, fetchClassesData, fetchClassesNoInstructorsData } from 'features/Dashboard/data';
import { executeThunk } from 'test-utils';
import { initializeStore } from 'store';
@@ -77,9 +77,58 @@ describe('Dashboard redux tests', () => {
.toEqual('error');
});
- test('successful fetch classes data', async () => {
+ test('successful fetch classesNoInstructors data', async () => {
+ const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/`
+ + '?limit=false&institution_id=1&course_name=&instructors=null';
+ const mockResponse = [
+ {
+ classId: 'ccx-v1:demo+demo1+2020+ccx1',
+ className: 'ccx 1',
+ masterCourseName: 'Demo Course 1',
+ instructors: [],
+ numberOfStudents: 0,
+ numberOfPendingStudents: 0,
+ maxStudents: 20,
+ startDate: '2024-01-23T21:50:51Z',
+ endDate: null,
+ },
+ ];
+ axiosMock.onGet(classesApiUrl)
+ .reply(200, mockResponse);
+
+ expect(store.getState().dashboard.classesNoInstructors.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchClassesNoInstructorsData(1), store.dispatch, store.getState);
+
+ expect(store.getState().dashboard.classesNoInstructors.data)
+ .toEqual(mockResponse);
+
+ expect(store.getState().dashboard.classesNoInstructors.status)
+ .toEqual('success');
+ });
+
+ test('failed fetch classesNoInstructors data', async () => {
const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/`
+ '?limit=false&institution_id=1&course_name=&instructors=null';
+ axiosMock.onGet(classesApiUrl)
+ .reply(500);
+
+ expect(store.getState().dashboard.classesNoInstructors.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchClassesNoInstructorsData(1), store.dispatch, store.getState);
+
+ expect(store.getState().dashboard.classesNoInstructors.data)
+ .toEqual([]);
+
+ expect(store.getState().dashboard.classesNoInstructors.status)
+ .toEqual('error');
+ });
+
+ test('successful fetch classes data', async () => {
+ const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/`
+ + '?limit=false&institution_id=1&course_name=&instructors=';
const mockResponse = [
{
classId: 'ccx-v1:demo+demo1+2020+ccx1',
@@ -108,9 +157,9 @@ describe('Dashboard redux tests', () => {
.toEqual('success');
});
- test('failed fetch licenses data', async () => {
+ test('failed fetch classes data', async () => {
const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/`
- + '?limit=false&institution_id=1&course_name=&instructors=null';
+ + '?limit=false&institution_id=1&course_name=&instructors=';
axiosMock.onGet(classesApiUrl)
.reply(500);
diff --git a/src/features/Dashboard/data/index.js b/src/features/Dashboard/data/index.js
index 910c1431..112aa236 100644
--- a/src/features/Dashboard/data/index.js
+++ b/src/features/Dashboard/data/index.js
@@ -1,2 +1,2 @@
export { reducer } from 'features/Dashboard/data/slice';
-export { fetchLicensesData, fetchClassesData } from 'features/Dashboard/data/thunks';
+export { fetchLicensesData, fetchClassesData, fetchClassesNoInstructorsData } from 'features/Dashboard/data/thunks';
diff --git a/src/features/Dashboard/data/slice.js b/src/features/Dashboard/data/slice.js
index 31adc748..9591c2f7 100644
--- a/src/features/Dashboard/data/slice.js
+++ b/src/features/Dashboard/data/slice.js
@@ -6,6 +6,9 @@ const initialState = {
tableLicense: {
...initialStateService,
},
+ classesNoInstructors: {
+ ...initialStateService,
+ },
classes: {
...initialStateService,
},
@@ -25,6 +28,16 @@ export const dashboardSlice = createSlice({
fetchLicensesDataFailed: (state) => {
state.tableLicense.status = RequestStatus.ERROR;
},
+ fetchClassesNoInstructorsDataRequest: (state) => {
+ state.classesNoInstructors.status = RequestStatus.LOADING;
+ },
+ fetchClassesNoInstructorsDataSuccess: (state, { payload }) => {
+ state.classesNoInstructors.status = RequestStatus.SUCCESS;
+ state.classesNoInstructors.data = payload;
+ },
+ fetchClassesNoInstructorsDataFailed: (state) => {
+ state.classesNoInstructors.status = RequestStatus.ERROR;
+ },
fetchClassesDataRequest: (state) => {
state.classes.status = RequestStatus.LOADING;
},
@@ -42,6 +55,9 @@ export const {
fetchLicensesDataRequest,
fetchLicensesDataSuccess,
fetchLicensesDataFailed,
+ fetchClassesNoInstructorsDataRequest,
+ fetchClassesNoInstructorsDataSuccess,
+ fetchClassesNoInstructorsDataFailed,
fetchClassesDataRequest,
fetchClassesDataSuccess,
fetchClassesDataFailed,
diff --git a/src/features/Dashboard/data/thunks.js b/src/features/Dashboard/data/thunks.js
index 9988ee98..f7ff7f09 100644
--- a/src/features/Dashboard/data/thunks.js
+++ b/src/features/Dashboard/data/thunks.js
@@ -6,6 +6,9 @@ import {
fetchLicensesDataRequest,
fetchLicensesDataSuccess,
fetchLicensesDataFailed,
+ fetchClassesNoInstructorsDataRequest,
+ fetchClassesNoInstructorsDataSuccess,
+ fetchClassesNoInstructorsDataFailed,
fetchClassesDataRequest,
fetchClassesDataSuccess,
fetchClassesDataFailed,
@@ -24,11 +27,24 @@ function fetchLicensesData(id) {
};
}
+function fetchClassesNoInstructorsData(id) {
+ return async (dispatch) => {
+ dispatch(fetchClassesNoInstructorsDataRequest());
+ try {
+ const response = camelCaseObject(await getClassesByInstitution(id, '', false, 'null'));
+ dispatch(fetchClassesNoInstructorsDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchClassesNoInstructorsDataFailed());
+ logError(error);
+ }
+ };
+}
+
function fetchClassesData(id) {
return async (dispatch) => {
dispatch(fetchClassesDataRequest());
try {
- const response = camelCaseObject(await getClassesByInstitution(id, '', false, 'null'));
+ const response = camelCaseObject(await getClassesByInstitution(id, '', false));
dispatch(fetchClassesDataSuccess(response.data));
} catch (error) {
dispatch(fetchClassesDataFailed());
@@ -40,4 +56,5 @@ function fetchClassesData(id) {
export {
fetchLicensesData,
fetchClassesData,
+ fetchClassesNoInstructorsData,
};
diff --git a/src/index.scss b/src/index.scss
index 211ae33e..bbabd7bd 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -7,3 +7,5 @@
@import "react-paragon-topaz/src/Form/Form.scss";
@import "react-paragon-topaz/src/Pagination/Pagination.scss";
@import "assets/global.scss";
+@import "react-date-range/dist/styles.css";
+@import "react-date-range/dist/theme/default.css";