diff --git a/backend/controllers/studentController.js b/backend/controllers/studentController.js index fcb209b..57614aa 100644 --- a/backend/controllers/studentController.js +++ b/backend/controllers/studentController.js @@ -155,6 +155,18 @@ export const deleteStudent = asyncHandler(async (req, res) => { const student = await Student.findById(req.params.id); if (student) { + + // Find associated User and delete it + const associatedUser = await User.findOne({ email: student.email }); + const associatedSchoolFees = await SchoolFees.findOne({student:student._id}) + + if (associatedUser) { + await associatedUser.remove(); + } + if (associatedUser) { + await associatedSchoolFees.remove() + } + await student.remove(); res.json({ success: true, message: 'Student removed' }); } else { diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js index c4dcbab..c42f0f2 100644 --- a/backend/controllers/userController.js +++ b/backend/controllers/userController.js @@ -310,6 +310,35 @@ const updateProfile = asyncHandler(async (req, res) => { } }); +// @desc Toggle user activation status +// @route PUT /api/users/:id/toggle-active +// @access Private/Admin +const toggleUserActivation = asyncHandler(async (req, res) => { + const userId = req.params.id; + + const user = await User.findById(userId); + + if (!user) { + res.status(404); + throw new Error('User not found'); + } + + user.isActive = !user.isActive; // Toggle isActive status + + const updatedUser = await user.save(); + + res.status(200).json({ + _id: updatedUser._id, + firstName: updatedUser.firstName, + secondName: updatedUser.secondName, + email: updatedUser.email, + isAdmin: updatedUser.isAdmin, + isActive: updatedUser.isActive, + userType: updatedUser.userType, + verified: updatedUser.verified, + }); +}); + export { authUser, registerUser, @@ -317,5 +346,6 @@ export { verifyResetPassword, setNewPassword, getAllUsers, - updateProfile + updateProfile, + toggleUserActivation } diff --git a/backend/models/studentModel.js b/backend/models/studentModel.js index 39ce3d6..456ff1d 100644 --- a/backend/models/studentModel.js +++ b/backend/models/studentModel.js @@ -80,6 +80,8 @@ const studentSchema = mongoose.Schema( } ); + + studentSchema.methods.matchPassword = async function (enteredPassword) { return await bcrypt.compare(enteredPassword, this.password); }; diff --git a/backend/routes/studentRoutes.js b/backend/routes/studentRoutes.js index 9ca2cb9..bf7b169 100644 --- a/backend/routes/studentRoutes.js +++ b/backend/routes/studentRoutes.js @@ -6,7 +6,7 @@ import { admitStudent, deleteStudent, getAllStudents, studentsByCourse } from '. student_router.route('/').post(protect, admin,admitStudent) student_router.route('/').get(protect,getAllStudents) -student_router.route('/').delete(protect,deleteStudent) +student_router.route('/:id').delete(protect,deleteStudent) student_router.route('/course/:id').get(protect,studentsByCourse) export default student_router diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 757222c..17a8185 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -7,7 +7,8 @@ import { authUser, sendRestPassword, getAllUsers, - updateProfile + updateProfile, + toggleUserActivation } from '../controllers/userController.js' import { protect, admin } from '../middleware/authMiddleware.js' @@ -17,7 +18,7 @@ router.route('/').get(getAllUsers) router.route('/reset-password').post(sendRestPassword) router.route('/change-password/:id/:token').post(setNewPassword) router.route('/update-profile').put(protect,updateProfile) - +router.route('/:id/toggle-active').put(protect, admin, toggleUserActivation); diff --git a/frontend/cypress/e2e/login.cy.js b/frontend/cypress/e2e/login.cy.js deleted file mode 100644 index a0de60a..0000000 --- a/frontend/cypress/e2e/login.cy.js +++ /dev/null @@ -1,81 +0,0 @@ -describe('Login Test Suite', () => { - beforeEach(() => { - - cy.visit('http://localhost:5173'); - }); - - it('should perform the login action successfully', () => { - - - cy.get('[data-cy="email"]').type('devngecu@gmail.com'); - cy.get('[data-cy="password"]').type('I@mrich254'); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(2000); - cy.location('pathname').should('eq', '/') - - }); - - it('Returns an error if email or password is empty', () => { - - - cy.get('[data-cy="email"]').type('caleb'); - // cy.get('[data-cy="password"]').type(''); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(500); - cy.contains('Form is invalid') - - }); - - it('Returns an error if email or password is missing', () => { - - - // cy.get('[data-cy="email"]').type('caleb'); - // cy.get('[data-cy="password"]').type(''); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(500); - cy.contains('Form is invalid') - - }); - - it('Returns an error if email is not in database', () => { - - - cy.get('[data-cy="email"]').type('kinuthia'); - cy.get('[data-cy="password"]').type('12345678'); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(500); - cy.contains('User not found') - - }); - - it('Handles incorrect password scenario', () => { - - - cy.get('[data-cy="email"]').type('caleb@gmail.com'); - cy.get('[data-cy="password"]').type('wrongPassword'); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(500); - cy.contains('Incorrect password') - - }); - - it('Check deactivated account', () => { - - - cy.get('[data-cy="email"]').type('caleb'); - cy.get('[data-cy="password"]').type('12345678'); - - cy.get('[data-cy="login-btn"]').click(); - cy.wait(2000); - - - cy.contains('Account deactivated, please contact admin') - - - }); - }); \ No newline at end of file diff --git a/frontend/cypress/e2e/spec.cy.js b/frontend/cypress/e2e/spec.cy.js deleted file mode 100644 index 322992c..0000000 --- a/frontend/cypress/e2e/spec.cy.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('template spec', () => { - it('passes', () => { - cy.visit('https://example.cypress.io') - }) -}) \ No newline at end of file diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json deleted file mode 100644 index 02e4254..0000000 --- a/frontend/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js deleted file mode 100644 index 66ea16e..0000000 --- a/frontend/cypress/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file diff --git a/frontend/cypress/support/e2e.js b/frontend/cypress/support/e2e.js deleted file mode 100644 index 0e7290a..0000000 --- a/frontend/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file diff --git a/frontend/src/actions/studentActions.jsx b/frontend/src/actions/studentActions.jsx index 199fde2..86d4277 100644 --- a/frontend/src/actions/studentActions.jsx +++ b/frontend/src/actions/studentActions.jsx @@ -10,6 +10,9 @@ import { STUDENT_DETAILS_REQUEST, STUDENT_DETAILS_SUCCESS, STUDENT_DETAILS_FAIL, + STUDENT_DELETE_REQUEST, + STUDENT_DELETE_SUCCESS, + STUDENT_DELETE_FAIL, } from '../constants/studentConstants'; import { useHistory } from 'react-router-dom'; const base_url = `http://localhost:5000/api/students`; diff --git a/frontend/src/actions/userActions.jsx b/frontend/src/actions/userActions.jsx index d957f7d..b5649df 100644 --- a/frontend/src/actions/userActions.jsx +++ b/frontend/src/actions/userActions.jsx @@ -30,7 +30,10 @@ import { USER_CHANGE_PASSWORD_REQUEST, USER_CHANGE_PASSWORD_SUCCESS, - USER_CHANGE_PASSWORD_FAIL + USER_CHANGE_PASSWORD_FAIL, + USER_TOGGLE_ACTIVE_REQUEST, + USER_TOGGLE_ACTIVE_SUCCESS, + USER_TOGGLE_ACTIVE_FAIL, } from '../constants/userConstants' import { useHistory } from 'react-router-dom'; @@ -303,4 +306,36 @@ export const listUsers = () => async (dispatch, getState) => { : error.message, }); } +}; + +export const toggleUserActive = (userId) => async (dispatch, getState) => { + try { + dispatch({ type: USER_TOGGLE_ACTIVE_REQUEST }); + + const { + userLogin: { userInfo }, + } = getState(); + + const config = { + headers: { + Authorization: `Bearer ${userInfo.token}`, + }, + }; + + + const { data } = await axios.put(`${base_url}/${userId}/toggle-active`, {}, config); + + dispatch({ + type: USER_TOGGLE_ACTIVE_SUCCESS, + payload: data, + }); + } catch (error) { + dispatch({ + type: USER_TOGGLE_ACTIVE_FAIL, + payload: + error.response && error.response.data.message + ? error.response.data.message + : error.message, + }); + } }; \ No newline at end of file diff --git a/frontend/src/constants/userConstants.jsx b/frontend/src/constants/userConstants.jsx index 63b2c97..d85cac5 100644 --- a/frontend/src/constants/userConstants.jsx +++ b/frontend/src/constants/userConstants.jsx @@ -37,4 +37,8 @@ export const USER_RESET_PASSWORD_FAIL = 'USER_RESET_PASSWORD_FAIL' export const USER_CHANGE_PASSWORD_REQUEST = 'USER_CHANGE_PASSWORD_REQUEST' export const USER_CHANGE_PASSWORD_SUCCESS = 'USER_CHANGE_PASSWORD_SUCCESS' -export const USER_CHANGE_PASSWORD_FAIL = 'USER_CHANGE_PASSWORD_FAIL' \ No newline at end of file +export const USER_CHANGE_PASSWORD_FAIL = 'USER_CHANGE_PASSWORD_FAIL' + +export const USER_TOGGLE_ACTIVE_REQUEST = 'USER_TOGGLE_ACTIVE_REQUEST'; +export const USER_TOGGLE_ACTIVE_SUCCESS = 'USER_TOGGLE_ACTIVE_SUCCESS'; +export const USER_TOGGLE_ACTIVE_FAIL = 'USER_TOGGLE_ACTIVE_FAIL'; diff --git a/frontend/src/reducers/userReducers.jsx b/frontend/src/reducers/userReducers.jsx index 1ed8c52..29ef806 100644 --- a/frontend/src/reducers/userReducers.jsx +++ b/frontend/src/reducers/userReducers.jsx @@ -30,7 +30,10 @@ import { USER_RESET_PASSWORD_FAIL, USER_CHANGE_PASSWORD_REQUEST, USER_CHANGE_PASSWORD_SUCCESS, - USER_CHANGE_PASSWORD_FAIL + USER_CHANGE_PASSWORD_FAIL, + USER_TOGGLE_ACTIVE_REQUEST, + USER_TOGGLE_ACTIVE_SUCCESS, + USER_TOGGLE_ACTIVE_FAIL, } from '../constants/userConstants' export const userLoginReducer = (state = {}, action) => { @@ -163,4 +166,20 @@ export const userChangePasswordReducer = (state = {}, action) => { default: return state } -} \ No newline at end of file +} + +export const userToggleActiveReducer = (state = {}, action) => { + switch (action.type) { + case USER_TOGGLE_ACTIVE_REQUEST: + return { loading: true }; + + case USER_TOGGLE_ACTIVE_SUCCESS: + return { loading: false, success: true, user: action.payload }; + + case USER_TOGGLE_ACTIVE_FAIL: + return { loading: false, error: action.payload }; + + default: + return state; + } +}; \ No newline at end of file diff --git a/frontend/src/screens/Admin/AllStudentsScreen.jsx b/frontend/src/screens/Admin/AllStudentsScreen.jsx index 4c51a58..e5207a7 100644 --- a/frontend/src/screens/Admin/AllStudentsScreen.jsx +++ b/frontend/src/screens/Admin/AllStudentsScreen.jsx @@ -3,13 +3,12 @@ import { Table, Form, Button, Row, Col, ListGroup, Container, Card, Pagination } import { useDispatch, useSelector } from 'react-redux'; import Message from '../../components/Message'; import Loader from '../../components/Loader'; -import { Link, useLocation } from 'react-router-dom'; -import { useRouteMatch } from 'react-router-dom'; -import { useHistory } from 'react-router-dom/cjs/react-router-dom.min'; -import { Collapse } from 'antd'; +import { useLocation } from 'react-router-dom'; + import Sidebar from './components/Sidebar' import { deleteStudent, listStudents } from '../../actions/studentActions'; import Topbar from './components/Topbar'; +import { Modal } from 'antd' @@ -24,12 +23,12 @@ const AllStudents = () => { const dispatch = useDispatch(); const [isModalOpen, setIsModalOpen] = useState(false); - const [lecturerData, setLecturerData] = useState(null); + const [studentData, setStudentData] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - - - const showModal = (lecturerData) => { - setLecturerData(lecturerData); // Assuming you have a state variable to store lecturer data + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + const showModal = (data) => { + setStudentData(data); setIsModalOpen(true); }; @@ -58,12 +57,12 @@ const AllStudents = () => { const studentList = useSelector((state) => state.studentList); const { loading, error, students } = studentList; - const lecturerDelete = useSelector((state) => state.lecturerDelete) + const studentDelete = useSelector((state) => state.studentDelete) const { loading: loadingDelete, error: errorDelete, success: successDelete, - } = lecturerDelete + } = studentDelete useEffect(() => { dispatch(listStudents()); @@ -81,40 +80,62 @@ const AllStudents = () => { const generateStudentData = () => { - - const filteredStudents = students.filter((student) => - student.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || - student.lastName.toLowerCase().includes(searchQuery.toLowerCase()) || - student.email.toLowerCase().includes(searchQuery.toLowerCase()) - // Add more fields as needed for searching - ); - - return filteredStudents.map((student) => ( - - - {student.firstName} {student.lastName} - {student.gender} - {student.course.name} - {student.email} - {student.dob} - - - - - - - - - )); + const filteredStudents = students.filter( + (student) => + student.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || + student.lastName.toLowerCase().includes(searchQuery.toLowerCase()) || + student.email.toLowerCase().includes(searchQuery.toLowerCase()) + // Add more fields as needed for searching + ); + + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredStudents.slice(indexOfFirstItem, indexOfLastItem); + + return ( + <> + {currentItems.map((student) => ( + + + {student.firstName} {student.lastName} +{student.gender} +{student.course.name} +{student.email} +{student.dob} + + + + + + + + + ))} + + {[...Array(Math.ceil(filteredStudents.length / itemsPerPage)).keys()].map( + (pageNumber) => ( + paginate(pageNumber + 1)} + > + {pageNumber + 1} + + ) + )} + + + ); }; + const paginate = (pageNumber) => setCurrentPage(pageNumber); return ( ); }; diff --git a/frontend/src/screens/Admin/AllTeachersScreen.jsx b/frontend/src/screens/Admin/AllTeachersScreen.jsx index 24bad00..071d8cb 100644 --- a/frontend/src/screens/Admin/AllTeachersScreen.jsx +++ b/frontend/src/screens/Admin/AllTeachersScreen.jsx @@ -11,6 +11,7 @@ import Sidebar from './components/Sidebar' import { deleteLecturer, listLecturers } from '../../actions/lecturerActions'; import Topbar from './components/Topbar'; import { Modal } from 'antd' +import { toggleUserActive } from '../../actions/userActions'; const AllLecturers = () => { @@ -19,10 +20,11 @@ const AllLecturers = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [lecturerData, setLecturerData] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(10); const showModal = (lecturerData) => { - setLecturerData(lecturerData); // Assuming you have a state variable to store lecturer data + setLecturerData(lecturerData); setIsModalOpen(true); }; @@ -34,7 +36,10 @@ const AllLecturers = () => { const handleCancel = () => { setIsModalOpen(false); }; - + + const toggleStatus = (userId)=>{ + dispatch(toggleUserActive(userId)) + } const deleteHandler = (lecturerID)=>{ console.log("i am deleting"); @@ -56,9 +61,14 @@ const AllLecturers = () => { success: successDelete, } = lecturerDelete + const userTogleActive = useSelector((state)=> state.userTogleActive) + const { + success:successToggle + } = userTogleActive + useEffect(() => { dispatch(listLecturers()); - }, [dispatch,successDelete]); + }, [dispatch,successDelete,successToggle]); if (loading) { return ; @@ -73,11 +83,14 @@ const AllLecturers = () => { lecturer.lecturer.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || lecturer.lecturer.lastName.toLowerCase().includes(searchQuery.toLowerCase()) || lecturer.lecturer.email.toLowerCase().includes(searchQuery.toLowerCase()) - // Add more fields as needed for searching + ); - - return filteredLecturers.map((lecturer) => ( + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentLecturers = filteredLecturers.slice(indexOfFirstItem, indexOfLastItem); + + return currentLecturers.map((lecturer) => ( @@ -86,8 +99,15 @@ const AllLecturers = () => { {lecturer.lecturer?.gender} {lecturer.lecturer?.school?.name} {lecturer.lecturer?.email} - {lecturer.lecturer?.dob} - {/* Add more lecturer-specific fields as needed */} + + {lecturer.user?.isActive ? ( + Active + ) : ( + Inactive + )} + + + + + {lecturer.user?.isActive ? ( + + ) : ( + + )} @@ -152,9 +182,9 @@ const AllLecturers = () => { Course Address - Date Of Birth + Active - E-mail + diff --git a/frontend/src/store.jsx b/frontend/src/store.jsx index 92103bf..3014254 100644 --- a/frontend/src/store.jsx +++ b/frontend/src/store.jsx @@ -12,6 +12,7 @@ import { userUpdateReducer, userResetPasswordReducer, userChangePasswordReducer, + userToggleActiveReducer, } from './reducers/userReducers'; import { @@ -130,6 +131,7 @@ const reducer = combineReducers({ userUpdate: userUpdateReducer, userResetPassword: userResetPasswordReducer, userChangePassword: userChangePasswordReducer, + userTogleActive:userToggleActiveReducer, // Course reducers courseCreate: courseCreateReducer,