Skip to content

Commit d9d390d

Browse files
committed
Implemented OCP and updated interfaces
1 parent a953bf1 commit d9d390d

File tree

3 files changed

+121
-61
lines changed

3 files changed

+121
-61
lines changed

src/pages/Courses/Course.tsx

Lines changed: 94 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Row as TRow } from "@tanstack/react-table";
22
import Table from "components/Table/Table";
33
import useAPI from "hooks/useAPI";
44
import { useCallback, useEffect, useMemo, useState } from "react";
5-
import { Button, Col, Container, Row } from "react-bootstrap";
5+
import { Button, Col, Container, Row, Tooltip } from "react-bootstrap";
66
import { RiHealthBookLine } from "react-icons/ri";
77
import { useDispatch, useSelector } from "react-redux";
88
import { Outlet, useLocation, useNavigate } from "react-router-dom";
@@ -12,17 +12,22 @@ import { ICourseResponse, ROLE } from "../../utils/interfaces";
1212
import { courseColumns as COURSE_COLUMNS } from "./CourseColumns";
1313
import CopyCourse from "./CourseCopy";
1414
import DeleteCourse from "./CourseDelete";
15-
import { formatDate, mergeDataAndNames } from "./CourseUtil";
15+
import { formatDate, mergeDataAndNamesAndInstructors } from "./CourseUtil";
16+
import { OverlayTrigger } from "react-bootstrap";
17+
18+
import { ICourseResponse as ICourse } from "../../utils/interfaces";
1619

1720
// Courses Component: Displays and manages courses, including CRUD operations.
1821

1922
/**
20-
* @author Atharva Thorve, on December, 2023
21-
* @author Mrityunjay Joshi on December, 2023
23+
@author Suraj Raghu Kumar, on Oct, 2024
24+
* @author Yuktasree Muppala on Oct, 2024
25+
* @author Harvardhan Patil on Oct, 2024
2226
*/
2327
const Courses = () => {
2428
const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI();
25-
const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI();
29+
const { data: InstitutionResponse, sendRequest: fetchInstitutions} = useAPI();
30+
const { data: InstructorResponse, sendRequest: fetchInstructors} = useAPI();
2631
const auth = useSelector(
2732
(state: RootState) => state.authentication,
2833
(prev, next) => prev.isAuthenticated === next.isAuthenticated
@@ -31,7 +36,19 @@ const Courses = () => {
3136
const location = useLocation();
3237
const dispatch = useDispatch();
3338

34-
// State for delete and copy confirmation modals
39+
// show course
40+
const [showDetailsModal, setShowDetailsModal] = useState<boolean>(false);
41+
const [selectedCourse, setSelectedCourse] = useState<ICourse | null>(null);
42+
43+
// Utility function to manage modals, adhering to Open-closed-principle
44+
const showModal = (setModalState: React.Dispatch<React.SetStateAction<boolean>>,
45+
setData?: (data: ICourse | null) => void, data?: ICourse) => {
46+
if (setData) {
47+
setData(data || null);
48+
}
49+
setModalState(true);
50+
};
51+
const handleShowDetails = (course: ICourse) => showModal(setShowDetailsModal, setSelectedCourse, course);
3552
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{
3653
visible: boolean;
3754
data?: ICourseResponse;
@@ -44,19 +61,13 @@ const Courses = () => {
4461

4562
useEffect(() => {
4663
// ToDo: Fix this API in backend so that it the institution name along with the id. Similar to how it is done in users.
47-
if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible) {
64+
if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible){
4865
fetchCourses({ url: `/courses` });
4966
// ToDo: Remove this API call later after the above ToDo is completed
5067
fetchInstitutions({ url: `/institutions` });
68+
fetchInstructors({ url: `/users` });
5169
}
52-
}, [
53-
fetchCourses,
54-
fetchInstitutions,
55-
location,
56-
showDeleteConfirmation.visible,
57-
auth.user.id,
58-
showCopyConfirmation.visible,
59-
]);
70+
}, [fetchCourses, fetchInstitutions,fetchInstructors, location, showDeleteConfirmation.visible, auth.user.id, showCopyConfirmation.visible]);
6071

6172
// Error alert for API errors
6273
useEffect(() => {
@@ -66,10 +77,7 @@ const Courses = () => {
6677
}, [error, dispatch]);
6778

6879
// Callbacks for handling delete and copy confirmation modals
69-
const onDeleteCourseHandler = useCallback(
70-
() => setShowDeleteConfirmation({ visible: false }),
71-
[]
72-
);
80+
const onDeleteCourseHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []);
7381

7482
const onCopyCourseHandler = useCallback(() => setShowCopyConfirmation({ visible: false }), []);
7583

@@ -85,17 +93,17 @@ const Courses = () => {
8593
);
8694

8795
const onDeleteHandle = useCallback(
88-
(row: TRow<ICourseResponse>) =>
89-
setShowDeleteConfirmation({ visible: true, data: row.original }),
96+
(row: TRow<ICourseResponse>) => setShowDeleteConfirmation({ visible: true, data: row.original }),
9097
[]
9198
);
9299

93100
const onCopyHandle = useCallback(
94101
(row: TRow<ICourseResponse>) => setShowCopyConfirmation({ visible: true, data: row.original }),
95102
[]
96103
);
97-
104+
98105
const tableColumns = useMemo(
106+
99107
() => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle),
100108
[onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle]
101109
);
@@ -110,58 +118,102 @@ const Courses = () => {
110118
[InstitutionResponse?.data, isLoading]
111119
);
112120

113-
tableData = mergeDataAndNames(tableData, institutionData);
121+
const instructorData = useMemo(
122+
() => (isLoading || !InstructorResponse?.data ? [] : InstructorResponse.data),
123+
[InstructorResponse?.data, isLoading]
124+
);
125+
126+
tableData = mergeDataAndNamesAndInstructors(tableData, institutionData, instructorData);
114127

115128
const formattedTableData = tableData.map((item: any) => ({
116129
...item,
117130
created_at: formatDate(item.created_at),
118131
updated_at: formatDate(item.updated_at),
119132
}));
120133

121-
// Render the Courses component
134+
// `auth.user.id` holds the ID of the logged-in user
135+
const loggedInUserId = auth.user.id;
136+
const loggedInUserRole = auth.user.role;
137+
138+
const visibleCourses = useMemo(() => {
139+
// Show all courses to admin and superadmin roles
140+
if (loggedInUserRole === ROLE.ADMIN.valueOf() || loggedInUserRole === ROLE.SUPER_ADMIN.valueOf()) {
141+
return formattedTableData;
142+
}
143+
// Otherwise, only show courses where the logged-in user is the instructor
144+
return formattedTableData.filter((CourseResponse: { instructor_id: number; }) => CourseResponse.instructor_id === loggedInUserId);
145+
}, [formattedTableData, loggedInUserRole]);
122146

147+
// Render the Courses component
148+
123149
return (
124150
<>
125151
<Outlet />
126152
<main>
127153
<Container fluid className="px-md-4">
128-
<Row className="mt-md-2 mb-md-2">
154+
<Row className="mt-4 mb-4">
129155
<Col className="text-center">
130-
<h1>Manage Courses</h1>
156+
<h1 className="text-dark" style={{ fontSize: '2rem', fontWeight: '600' }}>
157+
{auth.user.role === ROLE.INSTRUCTOR.valueOf() ? (
158+
<>Instructed by: {auth.user.full_name}</>
159+
) : auth.user.role === ROLE.TA.valueOf() ? (
160+
<>Assisted by: {auth.user.full_name}</>
161+
) : (
162+
<>Manage Courses</>
163+
)}
164+
</h1>
131165
</Col>
132166
<hr />
133167
</Row>
134-
<Row>
135-
<Col md={{ span: 1, offset: 11 }} style={{ paddingBottom: "10px" }}>
136-
<Button variant="outline-success" onClick={() => navigate("new")}>
137-
<RiHealthBookLine />
168+
169+
<Row className="mb-4 justify-content-end">
170+
<Col xs="auto">
171+
<Button
172+
variant="success"
173+
size="lg"
174+
onClick={() => navigate("new")}
175+
aria-label="Add New Course"
176+
style={{
177+
fontSize: '1rem',
178+
padding: '8px 24px',
179+
borderRadius: '10px',
180+
boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.1)',
181+
display: 'flex',
182+
alignItems: 'center',
183+
}}
184+
>
185+
<RiHealthBookLine style={{ marginRight: '8px', fontSize: '1.4rem' }} />
186+
Add Course
138187
</Button>
139188
</Col>
140-
{showDeleteConfirmation.visible && (
141-
<DeleteCourse
142-
courseData={showDeleteConfirmation.data!}
143-
onClose={onDeleteCourseHandler}
144-
/>
145-
)}
146-
{showCopyConfirmation.visible && (
147-
<CopyCourse courseData={showCopyConfirmation.data!} onClose={onCopyCourseHandler} />
148-
)}
149189
</Row>
190+
191+
{showDeleteConfirmation.visible && (
192+
<DeleteCourse courseData={showDeleteConfirmation.data!} onClose={onDeleteCourseHandler} />
193+
)}
194+
{showCopyConfirmation.visible && (
195+
<CopyCourse courseData={showCopyConfirmation.data!} onClose={onCopyCourseHandler} />
196+
)}
197+
150198
<Row>
151199
<Table
152200
showGlobalFilter={false}
153-
data={formattedTableData}
201+
data={visibleCourses}
154202
columns={tableColumns}
155203
columnVisibility={{
156204
id: false,
157205
institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(),
206+
instructor: auth.user.role === ROLE.SUPER_ADMIN.valueOf(),
158207
}}
159208
/>
160209
</Row>
161210
</Container>
162211
</main>
212+
213+
163214
</>
164-
);
215+
);
216+
165217
};
166218

167-
export default Courses;
219+
export default Courses;

src/pages/Courses/CourseUtil.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { IFormOption } from "components/Form/interfaces";
22
import { getPrivilegeFromID, hasAllPrivilegesOf } from "utils/util";
33
import axiosClient from "../../utils/axios_client";
4-
import { ICourseRequest, ICourseResponse, IInstitution, IInstitutionResponse, IInstructor, IUserRequest, ROLE } from "../../utils/interfaces";
4+
import { ICourseRequest, ICourseResponse, IInstitution, IInstitutionResponse,IInstructorResponse, IInstructor, IUserRequest, ROLE } from "../../utils/interfaces";
55

66
/**
7-
* @author Atharva Thorve, on December, 2023
8-
* @author Mrityunjay Joshi, on December, 2023
7+
* @author Aniket Singh Shaktawat, on March, 2024
8+
* @author Pankhi Saini on March, 2024
9+
* @author Siddharth Shah on March, 2024
910
*/
11+
1012
// Course Utility Functions and Constants
1113

1214
// Enumeration for course visibility options
@@ -145,21 +147,22 @@ export const formatDate = (dateString: string): string => {
145147
return new Intl.DateTimeFormat('en-US', options).format(date);
146148
};
147149

148-
// Function to merge data and names
149-
export const mergeDataAndNames = (data: ICourseResponse[], names: IInstitutionResponse[]): any => {
150+
// Function to merge course data with their respective institution and instructor data
151+
export const mergeDataAndNamesAndInstructors = (data: ICourseResponse[], institutionNames: IInstitutionResponse[], instructorNames: IInstructorResponse[]): any => {
150152
return data.map((dataObj) => {
151-
const matchingNameObject = names.find((nameObj) => nameObj.id === dataObj.institution_id);
152-
153-
if (matchingNameObject) {
154-
return {
155-
...dataObj,
156-
institution: {
157-
id: matchingNameObject.id,
158-
name: matchingNameObject.name,
159-
},
160-
};
161-
}
162-
163-
return dataObj;
153+
// Merge institution data
154+
const matchingInstitution = institutionNames.find((nameObj) => nameObj.id === dataObj.institution_id);
155+
const institutionData = matchingInstitution ? { id: matchingInstitution.id, name: matchingInstitution.name } : {};
156+
157+
// Merge instructor data
158+
const matchingInstructor = instructorNames.find((instructorObj) => instructorObj.id === dataObj.instructor_id);
159+
const instructorData = matchingInstructor ? { id: matchingInstructor.id, name: matchingInstructor.name } : {};
160+
161+
// Merge course data with institution and instructor data
162+
return {
163+
...dataObj,
164+
institution: institutionData,
165+
instructor: instructorData
166+
};
164167
});
165-
};
168+
};

src/utils/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ export interface IInstitutionResponse {
142142
name: string;
143143
}
144144

145+
export interface IInstructorResponse {
146+
id: number;
147+
name: string;
148+
}
149+
145150
export enum ROLE {
146151
SUPER_ADMIN = "Super Administrator",
147152
ADMIN = "Administrator",

0 commit comments

Comments
 (0)