├── client ├── src │ ├── App.css │ ├── setupTests.js │ ├── App.test.js │ ├── components │ │ ├── PublicRoute.js │ │ ├── Spinner.js │ │ ├── DoctorList.js │ │ ├── ProtectedRoute.js │ │ └── Layout.js │ ├── redux │ │ ├── store.js │ │ └── features │ │ │ ├── userSlice.js │ │ │ └── alertSlice.js │ ├── reportWebVitals.js │ ├── index.js │ ├── Data │ │ └── data.js │ ├── pages │ │ ├── HomePage.js │ │ ├── Appointments.js │ │ ├── admin │ │ │ ├── Users.js │ │ │ └── Doctors.js │ │ ├── Login.js │ │ ├── doctor │ │ │ ├── DoctorAppointments.js │ │ │ └── Profile.js │ │ ├── Register.js │ │ ├── BookingPage.js │ │ ├── NotificationPage.js │ │ └── ApplyDoctor.js │ ├── index.css │ ├── logo.svg │ ├── styles │ │ └── LayoutStyles.css │ └── App.js ├── build │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── asset-manifest.json │ ├── manifest.json │ ├── index.html │ └── static │ │ ├── js │ │ └── main.63fc4e3b.js.LICENSE.txt │ │ └── css │ │ ├── main.9cc7b55e.css │ │ └── main.9cc7b55e.css.map ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore └── package.json ├── .gitignore ├── Images ├── Doctor Homepage.png ├── Homepage for Admin.png ├── Homepage from User.png ├── Manage Doctor Profile.png ├── Users List from Admin.png ├── User can apply for doctor.png ├── After Read Notification Page.png ├── Booking Appointment as User.png ├── Doctor List from admin page.png ├── Appointment Lists from User Profile.png ├── Appointment Lists from Doctor Profile.png └── Notification Page for New Notifications.png ├── desktop.ini ├── vercel.json ├── package.json ├── config └── connectDb.js ├── middlewares └── authMiddleware.js ├── routes ├── adminRoute.js ├── doctorRoute.js └── userRoute.js ├── index.js ├── models ├── appointmentModel.js ├── doctorModel.js └── userModel.js ├── controllers ├── adminController.js ├── doctorController.js └── userController.js └── README.md /client/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/build/logo192.png -------------------------------------------------------------------------------- /client/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/build/logo512.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /Images/Doctor Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Doctor Homepage.png -------------------------------------------------------------------------------- /Images/Homepage for Admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Homepage for Admin.png -------------------------------------------------------------------------------- /Images/Homepage from User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Homepage from User.png -------------------------------------------------------------------------------- /Images/Manage Doctor Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Manage Doctor Profile.png -------------------------------------------------------------------------------- /Images/Users List from Admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Users List from Admin.png -------------------------------------------------------------------------------- /Images/User can apply for doctor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/User can apply for doctor.png -------------------------------------------------------------------------------- /Images/After Read Notification Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/After Read Notification Page.png -------------------------------------------------------------------------------- /Images/Booking Appointment as User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Booking Appointment as User.png -------------------------------------------------------------------------------- /Images/Doctor List from admin page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Doctor List from admin page.png -------------------------------------------------------------------------------- /desktop.ini: -------------------------------------------------------------------------------- 1 | [.ShellClassInfo] 2 | IconResource=C:\Windows\System32\SHELL32.dll,23 3 | [ViewState] 4 | Mode= 5 | Vid= 6 | FolderType=Generic 7 | -------------------------------------------------------------------------------- /Images/Appointment Lists from User Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Appointment Lists from User Profile.png -------------------------------------------------------------------------------- /Images/Appointment Lists from Doctor Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Appointment Lists from Doctor Profile.png -------------------------------------------------------------------------------- /Images/Notification Page for New Notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OviSarkar62/AppointDoc/HEAD/Images/Notification Page for New Notifications.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "builds": [ 3 | { 4 | "src": "./index.js", 5 | "use": "@vercel/node" 6 | } 7 | ], 8 | "routes": [ 9 | { 10 | "src": "/.*", 11 | "dest": "index.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/components/PublicRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | 4 | export default function PublicRoute({ children }) { 5 | if (localStorage.getItem("token")) { 6 | return ; 7 | } else { 8 | return children; 9 | } 10 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | 10 | 11 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import { alertSlice } from "./features/alertSlice"; 3 | import { userSlice } from "./features/userSlice"; 4 | 5 | export default configureStore({ 6 | reducer: { 7 | alerts: alertSlice.reducer, 8 | user: userSlice.reducer, 9 | }, 10 | }); -------------------------------------------------------------------------------- /client/src/redux/features/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const userSlice = createSlice({ 4 | name: "user", 5 | initialState: { 6 | user: null, 7 | }, 8 | reducers: { 9 | setUser: (state, action) => { 10 | state.user = action.payload; 11 | }, 12 | }, 13 | }); 14 | 15 | export const { setUser } = userSlice.actions; -------------------------------------------------------------------------------- /client/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Spinners from "react-bootstrap/Spinner"; 3 | 4 | const Spinner = () => { 5 | return ( 6 | <> 7 |
8 | 9 | Loading... 10 |
11 | 12 | ); 13 | }; 14 | 15 | export default Spinner; 16 | -------------------------------------------------------------------------------- /client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.9cc7b55e.css", 4 | "main.js": "/static/js/main.63fc4e3b.js", 5 | "index.html": "/index.html", 6 | "main.9cc7b55e.css.map": "/static/css/main.9cc7b55e.css.map", 7 | "main.63fc4e3b.js.map": "/static/js/main.63fc4e3b.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.9cc7b55e.css", 11 | "static/js/main.63fc4e3b.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import "antd/dist/reset.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import { Provider } from "react-redux"; 5 | import App from "./App"; 6 | import "./index.css"; 7 | import store from "./redux/store"; 8 | 9 | 10 | const root = ReactDOM.createRoot(document.getElementById("root")); 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appointdoctor", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcrypt": "^5.1.0", 14 | "bcryptjs": "^2.4.3", 15 | "colors": "^1.4.0", 16 | "dotenv": "^16.0.3", 17 | "express": "^4.18.2", 18 | "joi": "^17.9.2", 19 | "jsonwebtoken": "^9.0.0", 20 | "moment": "^2.29.4", 21 | "mongoose": "^7.1.0", 22 | "morgan": "^1.10.0", 23 | "nodemon": "^2.0.22", 24 | "zxcvbn": "^4.4.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/connectDb.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const colors = require("colors"); 3 | 4 | const connectDb = async () => { 5 | try { 6 | if (!process.env.DB_URL) { 7 | throw new Error("DB_URL environment variable not set"); 8 | } 9 | 10 | await mongoose.connect(process.env.DB_URL, { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | }); 14 | 15 | console.log(`Server running on ${mongoose.connection.host}`.bgCyan.white); 16 | } catch (error) { 17 | console.error(`Error connecting to database: ${error}`.bgRed); 18 | process.exit(1); 19 | } 20 | }; 21 | 22 | module.exports = connectDb; -------------------------------------------------------------------------------- /middlewares/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const JWT = require("jsonwebtoken"); 2 | 3 | module.exports = async (req, res, next) => { 4 | try { 5 | const token = req.headers["authorization"].split(" ")[1]; 6 | JWT.verify(token, process.env.JWT_SECRET, (err, decode) => { 7 | if (err) { 8 | return res.status(200).send({ 9 | message: "Auth Fialed", 10 | success: false, 11 | }); 12 | } else { 13 | req.body.userId = decode.id; 14 | next(); 15 | } 16 | }); 17 | } catch (error) { 18 | console.log(error); 19 | res.status(401).send({ 20 | message: "Auth Failed", 21 | success: false, 22 | }); 23 | } 24 | }; -------------------------------------------------------------------------------- /routes/adminRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | getAllUsersController, 4 | getAllDoctorsController, 5 | changeAccountStatusController, 6 | } = require("../controllers/adminController"); 7 | const authMiddleware = require("../middlewares/authMiddleware"); 8 | 9 | 10 | const router = express.Router(); 11 | 12 | //GET METHOD || USERS 13 | router.get("/getAllUsers", authMiddleware, getAllUsersController); 14 | 15 | //GET METHOD || DOCTORS 16 | router.get("/getAllDoctors", authMiddleware, getAllDoctorsController); 17 | 18 | //POST ACCOUNT STATUS 19 | router.post("/changeAccountStatus", authMiddleware, changeAccountStatusController); 20 | 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /client/src/Data/data.js: -------------------------------------------------------------------------------- 1 | export const userMenu = [ 2 | { 3 | name: "Home", 4 | path: "/", 5 | icon: "fa-solid fa-house", 6 | }, 7 | { 8 | name: "Appointments", 9 | path: "/appointments", 10 | icon: "fa-solid fa-list", 11 | }, 12 | { 13 | name: "Apply Doctor", 14 | path: "/apply-doctor", 15 | icon: "fa-solid fa-user-doctor", 16 | } 17 | ]; 18 | 19 | // admin menu 20 | export const adminMenu = [ 21 | { 22 | name: "Home", 23 | path: "/", 24 | icon: "fa-solid fa-house", 25 | }, 26 | 27 | { 28 | name: "Doctors", 29 | path: "/admin/doctors", 30 | icon: "fa-solid fa-user-doctor", 31 | }, 32 | { 33 | name: "Users", 34 | path: "/admin/users", 35 | icon: "fa-solid fa-user", 36 | } 37 | ]; 38 | 39 | -------------------------------------------------------------------------------- /client/src/redux/features/alertSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const alertSlice = createSlice({ 4 | name: "alerts", 5 | initialState: { 6 | loading: false, 7 | user: { 8 | notification: [], 9 | seennotification: [], 10 | }, 11 | }, 12 | reducers: { 13 | showLoading: (state) => { 14 | state.loading = true; 15 | }, 16 | hideLoading: (state) => { 17 | state.loading = false; 18 | }, 19 | setUserNotification: (state, action) => { 20 | state.user.notification = action.payload; 21 | }, 22 | setSeenNotification: (state, action) => { 23 | state.user.seennotification = action.payload; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { 29 | showLoading, 30 | hideLoading, 31 | setUserNotification, 32 | setSeenNotification, 33 | } = alertSlice.actions; 34 | 35 | 36 | -------------------------------------------------------------------------------- /routes/doctorRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | getDoctorInfoController, 4 | updateProfileController, 5 | getDoctorByIdController, 6 | doctorAppointmentsController, 7 | updateStatusController, 8 | } = require("../controllers/doctorController"); 9 | const authMiddleware = require("../middlewares/authMiddleware"); 10 | const router = express.Router(); 11 | 12 | //POST SINGLE DOC INFO 13 | router.post("/getDoctorInfo", authMiddleware, getDoctorInfoController); 14 | 15 | //POST UPDATE PROFILE 16 | router.post("/updateProfile", authMiddleware, updateProfileController); 17 | 18 | //POST GET SINGLE DOC INFO 19 | router.post("/getDoctorById", authMiddleware, getDoctorByIdController); 20 | 21 | //GET Appointments 22 | router.get("/doctor-appointments", authMiddleware, doctorAppointmentsController); 23 | 24 | //POST Update Status 25 | router.post("/update-status", authMiddleware, updateStatusController); 26 | 27 | module.exports = router; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const colors = require("colors"); 3 | const moragan = require("morgan"); 4 | const dotenv = require("dotenv"); 5 | const userRoutes = require('./routes/userRoute'); 6 | const adminRoutes = require("./routes/adminRoute"); 7 | const doctorRoutes = require("./routes/doctorRoute") 8 | const connectDb = require("./config/connectDb"); 9 | const path = require("path"); 10 | 11 | //dotenv conig 12 | dotenv.config(); 13 | connectDb(); 14 | 15 | //rest obejct 16 | const app = express(); 17 | 18 | //middlewares 19 | app.use(express.json()); 20 | app.use(moragan("dev")); 21 | 22 | //routes 23 | app.use("/api/user", userRoutes); 24 | app.use("/api/admin", adminRoutes); 25 | app.use("/api/doctor", doctorRoutes); 26 | 27 | //static files 28 | app.use(express.static(path.join(__dirname, "./client/build"))); 29 | 30 | app.get("*", function (req, res) { 31 | res.sendFile(path.join(__dirname, "./client/build/index.html")); 32 | }); 33 | 34 | //port 35 | const PORT = process.env.PORT || 4001; 36 | //listen port 37 | app.listen(PORT, () => { 38 | console.log(`Server running on http://localhost:${PORT}`); 39 | }); -------------------------------------------------------------------------------- /client/src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import { Row } from "antd"; 2 | import axios from "axios"; 3 | import React, { useEffect, useState } from "react"; 4 | import DoctorList from "../components/DoctorList"; 5 | import Layout from "./../components/Layout"; 6 | 7 | const HomePage = () => { 8 | const [doctors, setDoctors] = useState([]); 9 | // login user data 10 | const getUserData = async () => { 11 | try { 12 | const res = await axios.get( 13 | "/api/user/getAllDoctors", 14 | 15 | { 16 | headers: { 17 | Authorization: "Bearer " + localStorage.getItem("token"), 18 | }, 19 | } 20 | ); 21 | if (res.data.success) { 22 | setDoctors(res.data.data); 23 | } 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | getUserData(); 31 | }, []); 32 | return ( 33 | 34 |

Home Page

35 |
36 | 37 | {doctors && doctors.map((doctor) => )} 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default HomePage; -------------------------------------------------------------------------------- /client/src/components/DoctorList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | const DoctorList = ({ doctor }) => { 5 | const navigate = useNavigate(); 6 | return ( 7 | <> 8 |
navigate(`/doctor/book-appointment/${doctor._id}`)} 12 | > 13 |
17 | Dr. {doctor.firstName} {doctor.lastName} 18 |
19 | 20 |
21 |

22 | Specialization: {doctor.specialization} 23 |

24 |

25 | Experience: {doctor.experience} 26 |

27 |

28 | Fees Per Consultation: {doctor.feesPerConsultation} 29 |

30 |

31 | Timings: {doctor.starttime} - {doctor.endtime} 32 |

33 |
34 |
35 | 36 | ); 37 | }; 38 | 39 | export default DoctorList; 40 | -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | Appoint Doctor
-------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "proxy": "http://localhost:4000", 3 | "name": "client", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.9.5", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "antd": "^5.4.6", 12 | "axios": "^1.4.0", 13 | "bootstrap": "^5.2.3", 14 | "moment": "^2.29.4", 15 | "react": "^18.2.0", 16 | "react-bootstrap": "^2.7.4", 17 | "react-dom": "^18.2.0", 18 | "react-redux": "^8.0.5", 19 | "react-router-dom": "^6.11.0", 20 | "react-scripts": "5.0.1", 21 | "react-spinners": "^0.13.8", 22 | "web-vitals": "^2.1.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | z-index: 100; 7 | } 8 | 9 | .spinner-border { 10 | height: 50px; 11 | width: 50px; 12 | } 13 | 14 | /*register page*/ 15 | 16 | .register-page { 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | height: 90vh; 21 | } 22 | 23 | .register-page form { 24 | width: 400px; 25 | background-color: #fff; 26 | padding: 40px; 27 | border-radius: 10px; 28 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 29 | } 30 | .register-page form .d-flex { 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | margin-top: 20px; 35 | } 36 | 37 | .register-page form .d-flex a { 38 | color: #007bff; 39 | text-decoration: none; 40 | } 41 | 42 | .register-page h1 { 43 | text-align: center; 44 | margin-bottom: 20px; 45 | } 46 | 47 | .register-page .ant-form-item { 48 | margin-bottom: 20px; 49 | } 50 | 51 | .register-page .ant-form-item-label { 52 | font-weight: bold; 53 | } 54 | 55 | .register-page .ant-form-item-explain { 56 | color: #ff4d4f; 57 | } 58 | 59 | .register-page .ant-form-item-has-error .ant-input { 60 | border-color: #ff4d4f; 61 | } 62 | 63 | .register-page .ant-form-item-has-error .ant-input:focus { 64 | box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); 65 | } 66 | 67 | .register-page .btn-primary { 68 | width: 100%; 69 | } 70 | -------------------------------------------------------------------------------- /client/src/pages/Appointments.js: -------------------------------------------------------------------------------- 1 | import { Table } from "antd"; 2 | import axios from "axios"; 3 | import moment from "moment"; 4 | import React, { useEffect, useState } from "react"; 5 | import Layout from "./../components/Layout"; 6 | 7 | 8 | const Appointments = () => { 9 | const [appointments, setAppointments] = useState([]); 10 | 11 | const getAppointments = async () => { 12 | try { 13 | const res = await axios.get("/api/user/user-appointments", { 14 | headers: { 15 | Authorization: `Bearer ${localStorage.getItem("token")}`, 16 | }, 17 | }); 18 | if (res.data.success) { 19 | setAppointments(res.data.data); 20 | } 21 | } catch (error) { 22 | console.log(error); 23 | } 24 | }; 25 | 26 | useEffect(() => { 27 | getAppointments(); 28 | }, []); 29 | 30 | const columns = [ 31 | { 32 | title: "ID", 33 | dataIndex: "_id", 34 | }, 35 | { 36 | title: "Date & Time", 37 | dataIndex: "date", 38 | render: (text, record) => ( 39 | 40 | {moment(record.date).format("DD-MM-YYYY")}   41 | {moment(record.time).format("HH:mm")} 42 | 43 | ), 44 | }, 45 | { 46 | title: "Status", 47 | dataIndex: "status", 48 | }, 49 | ]; 50 | 51 | return ( 52 | 53 |

Appointments Lists

54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Appointments; 60 | -------------------------------------------------------------------------------- /models/appointmentModel.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const mongoose = require('mongoose'); 3 | 4 | const appointmentSchema = new mongoose.Schema( 5 | { 6 | userId: { 7 | type: String, 8 | required: true, 9 | }, 10 | doctorId: { 11 | type: String, 12 | required: true, 13 | }, 14 | doctorInfo: { 15 | type: String, 16 | required: true, 17 | }, 18 | userInfo: { 19 | type: String, 20 | required: true, 21 | }, 22 | date: { 23 | type: Date, // changed type to Date 24 | required: true, 25 | }, 26 | status: { 27 | type: String, 28 | required: true, 29 | default: 'pending', 30 | }, 31 | time: { 32 | type: String, 33 | required: true, 34 | }, 35 | }, 36 | { timestamps: true } 37 | ); 38 | 39 | 40 | // Define a Joi schema for appointment validation 41 | const appointmentJoiSchema = Joi.object({ 42 | userId: Joi.string().required(), 43 | doctorId: Joi.string().required(), 44 | doctorInfo: Joi.string().required(), 45 | userInfo: Joi.string().required(), 46 | date: Joi.date().required(), 47 | status: Joi.string().required().default('pending'), 48 | time: Joi.string().required(), 49 | }).options({ stripUnknown: true }); 50 | 51 | 52 | const AppointmentModel = mongoose.model('appointments', appointmentSchema); 53 | 54 | // Add the Joi validation to the Mongoose schema 55 | AppointmentModel.validateAppointment = async function () { 56 | return appointmentJoiSchema.validateAsync(this.toObject()); 57 | }; 58 | 59 | module.exports = AppointmentModel; 60 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 20 | 27 | Appoint Doctor 28 | 29 | 30 | 31 |
32 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/src/pages/admin/Users.js: -------------------------------------------------------------------------------- 1 | import { Table } from "antd"; 2 | import axios from "axios"; 3 | import React, { useEffect, useState } from "react"; 4 | import Layout from "./../../components/Layout"; 5 | const Users = () => { 6 | const [users, setUsers] = useState([]); 7 | 8 | //getUsers 9 | const getUsers = async () => { 10 | try { 11 | const res = await axios.get("/api/admin/getAllUsers", { 12 | headers: { 13 | Authorization: `Bearer ${localStorage.getItem("token")}`, 14 | }, 15 | }); 16 | if (res.data.success) { 17 | setUsers(res.data.data); 18 | } 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | }; 23 | 24 | useEffect(() => { 25 | getUsers(); 26 | }, []); 27 | 28 | // antD table col 29 | const columns = [ 30 | { 31 | title: "Name", 32 | dataIndex: "name", 33 | }, 34 | { 35 | title: "Email", 36 | dataIndex: "email", 37 | }, 38 | { 39 | title: "Doctor", 40 | dataIndex: "isDoctor", 41 | render: (text, record) => {record.isDoctor ? "Yes" : "No"}, 42 | }, 43 | { 44 | title: "Actions", 45 | dataIndex: "actions", 46 | render: (text, record) => ( 47 |
48 | 49 |
50 | ), 51 | }, 52 | ]; 53 | 54 | return ( 55 | 56 |

Users List

57 |
58 | 59 | ); 60 | }; 61 | 62 | export default Users; -------------------------------------------------------------------------------- /routes/userRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | loginController, 4 | registerController, 5 | authController, 6 | applyDoctorController, 7 | getAllNotificationController, 8 | deleteAllNotificationController, 9 | getAllDocotrsController, 10 | bookAppointmentController, 11 | bookingAvailabilityController, 12 | userAppointmentsController, 13 | } = require("../controllers/userController"); 14 | const authMiddleware = require("../middlewares/authMiddleware"); 15 | 16 | //router object 17 | const router = express.Router(); 18 | 19 | //routes 20 | // POST || LOGIN USER 21 | router.post("/login", loginController); 22 | 23 | //POST || REGISTER USER 24 | router.post("/register", registerController); 25 | 26 | //Auth || POST 27 | router.post("/getUserData", authMiddleware, authController); 28 | 29 | //Apply Doctor || POST 30 | router.post("/apply-doctor", authMiddleware, applyDoctorController); 31 | 32 | //Notifiaction Doctor || POST 33 | router.post("/get-all-notification", authMiddleware, getAllNotificationController); 34 | 35 | //Notifiaction Doctor || POST 36 | router.post("/delete-all-notification", authMiddleware, deleteAllNotificationController); 37 | 38 | //GET ALL DOC 39 | router.get("/getAllDoctors", authMiddleware, getAllDocotrsController); 40 | 41 | //BOOK APPOINTMENT 42 | router.post("/book-appointment", authMiddleware, bookAppointmentController); 43 | 44 | //Booking Avliability 45 | router.post("/booking-availbility", authMiddleware, bookingAvailabilityController); 46 | 47 | //Appointments List 48 | router.get("/user-appointments", authMiddleware, userAppointmentsController); 49 | 50 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import React, { useEffect } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { Navigate } from "react-router-dom"; 5 | import { hideLoading, showLoading } from "../redux/features/alertSlice"; 6 | import { setUser } from "../redux/features/userSlice"; 7 | 8 | 9 | export default function ProtectedRoute({ children }) { 10 | const dispatch = useDispatch(); 11 | const { user } = useSelector((state) => state.user); 12 | 13 | useEffect(() => { 14 | const getUser = async () => { 15 | try { 16 | dispatch(showLoading()); 17 | const {data} = await axios.post( 18 | "/api/user/getUserData", 19 | { token: localStorage.getItem("token") }, 20 | { 21 | headers: { 22 | Authorization: `Bearer ${localStorage.getItem("token")}`, 23 | }, 24 | } 25 | ); 26 | dispatch(hideLoading()); 27 | if (data.success) { 28 | dispatch(setUser(data.data)); 29 | } else { 30 | localStorage.clear(); 31 | ; 32 | } 33 | } catch (error) { 34 | localStorage.clear(); 35 | dispatch(hideLoading()); 36 | console.log(error); 37 | } 38 | }; 39 | 40 | if (!user) { 41 | getUser(); 42 | } 43 | }, [user, dispatch]); 44 | 45 | if (localStorage.getItem("token")) { 46 | return children; 47 | } else { 48 | return ; 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /client/src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import { Form, Input, message } from "antd"; 2 | import axios from "axios"; 3 | import React from "react"; 4 | import { useDispatch } from "react-redux"; 5 | import { Link, useNavigate } from "react-router-dom"; 6 | import { hideLoading, showLoading } from "../redux/features/alertSlice"; 7 | 8 | const Login = () => { 9 | const dispatch = useDispatch(); 10 | const navigate = useNavigate(); 11 | 12 | // Submit for Login 13 | const submitHandler = async (values) => { 14 | try { 15 | dispatch(showLoading()); 16 | const { data } = await axios.post("/api/user/login", values); 17 | window.location.reload(); 18 | dispatch(hideLoading()); 19 | if (data.success) { 20 | localStorage.setItem("token", data.token); 21 | message.success("Login Successfully"); 22 | navigate("/"); 23 | } else { 24 | message.error(data.message); 25 | } 26 | } catch (error) { 27 | dispatch(hideLoading()); 28 | message.error("Something went wrong"); 29 | } 30 | }; 31 | 32 | return ( 33 | <> 34 |
35 |
36 |

Login

37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | Not a User? Click Here to Register 45 | 46 |
47 | 48 |
49 | 50 | ); 51 | }; 52 | 53 | export default Login; -------------------------------------------------------------------------------- /controllers/adminController.js: -------------------------------------------------------------------------------- 1 | const doctorModel = require('../models/doctorModel'); 2 | const userModel = require("../models/userModel"); 3 | 4 | 5 | const getAllUsersController = async (req, res) => { 6 | try { 7 | const users = await userModel.find({}); 8 | res.status(200).send({ 9 | success: true, 10 | message: "Users Data List", 11 | data: users, 12 | }); 13 | } catch (error) { 14 | console.log(error); 15 | res.status(500).send({ 16 | success: false, 17 | message: "Error While Fetching Users", 18 | error, 19 | }); 20 | } 21 | }; 22 | 23 | //GET ALL DOC 24 | const getAllDoctorsController = async (req, res) => { 25 | try { 26 | const doctors = await doctorModel.find({}); 27 | res.status(200).send({ 28 | success: true, 29 | message: "Doctors Data list", 30 | data: doctors, 31 | }); 32 | } catch (error) { 33 | console.log(error); 34 | res.status(500).send({ 35 | success: false, 36 | message: "Error While Getting Doctors Data", 37 | error, 38 | }); 39 | } 40 | }; 41 | 42 | 43 | // doctor account status 44 | const changeAccountStatusController = async (req, res) => { 45 | try { 46 | const { doctorId, status } = req.body; 47 | const doctor = await doctorModel.findByIdAndUpdate(doctorId, { status }); 48 | const user = await userModel.findOne({ _id: doctor.userId }); 49 | const notification = user.notification; 50 | notification.push({ 51 | type: "doctor-account-request-updated", 52 | message: `Your Doctor Account Request Has ${status} `, 53 | onClickPath: "/notification", 54 | }); 55 | user.isDoctor = status === "approved" ? true : false; 56 | await user.save(); 57 | res.status(201).send({ 58 | success: true, 59 | message: "Account Status Updated", 60 | data: doctor, 61 | }); 62 | } catch (error) { 63 | console.log(error); 64 | res.status(500).send({ 65 | success: false, 66 | message: "Error In Account Status", 67 | error, 68 | }); 69 | } 70 | }; 71 | 72 | 73 | module.exports = { getAllDoctorsController, getAllUsersController, changeAccountStatusController }; -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/pages/admin/Doctors.js: -------------------------------------------------------------------------------- 1 | import { Table, message } from "antd"; 2 | import axios from "axios"; 3 | import React, { useEffect, useState } from "react"; 4 | import Layout from "./../../components/Layout"; 5 | 6 | const Doctors = () => { 7 | const [doctors, setDoctors] = useState([]); 8 | //getUsers 9 | const getDoctors = async () => { 10 | try { 11 | const res = await axios.get("/api/admin/getAllDoctors", { 12 | headers: { 13 | Authorization: `Bearer ${localStorage.getItem("token")}`, 14 | }, 15 | }); 16 | if (res.data.success) { 17 | setDoctors(res.data.data); 18 | } 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | }; 23 | 24 | // handle account 25 | const handleAccountStatus = async (record, status) => { 26 | try { 27 | const res = await axios.post( 28 | "/api/admin/changeAccountStatus", 29 | { doctorId: record._id, userId: record.userId, status: status }, 30 | { 31 | headers: { 32 | Authorization: `Bearer ${localStorage.getItem("token")}`, 33 | }, 34 | } 35 | ); 36 | if (res.data.success) { 37 | message.success(res.data.message); 38 | window.location.reload(); 39 | } 40 | } catch (error) { 41 | message.error("Something Went Wrong"); 42 | } 43 | }; 44 | 45 | useEffect(() => { 46 | getDoctors(); 47 | }, []); 48 | 49 | const columns = [ 50 | { 51 | title: "Name", 52 | dataIndex: "name", 53 | render: (text, record) => ( 54 | 55 | {record.firstName} {record.lastName} 56 | 57 | ), 58 | }, 59 | { 60 | title: "Status", 61 | dataIndex: "status", 62 | }, 63 | { 64 | title: "Phone", 65 | dataIndex: "phone", 66 | }, 67 | { 68 | title: "Actions", 69 | dataIndex: "actions", 70 | render: (text, record) => ( 71 |
72 | {record.status === "pending" ? ( 73 | 79 | ) : ( 80 | 81 | )} 82 |
83 | ), 84 | }, 85 | ]; 86 | 87 | return ( 88 | 89 |

All Doctors

90 |
91 | 92 | ); 93 | }; 94 | 95 | export default Doctors; -------------------------------------------------------------------------------- /models/doctorModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Joi = require('joi'); 3 | 4 | const doctorSchema = new mongoose.Schema({ 5 | userId: { 6 | type: String, 7 | }, 8 | firstName: { 9 | type: String, 10 | required: [true, "First name is required"], 11 | minlength: [2, 'Your first name must be at least 2 characters'], 12 | maxlength: [50, 'Your first name cannot exceed 50 characters'], 13 | }, 14 | lastName: { 15 | type: String, 16 | required: [true, "Last name is required"], 17 | minlength: [2, 'Your last name must be at least 2 characters'], 18 | maxlength: [50, 'Your last name cannot exceed 50 characters'], 19 | }, 20 | phone: { 21 | type: String, 22 | required: [true, "Phone number is required"], 23 | }, 24 | email: { 25 | type: String, 26 | required: [true, "Email is required"], 27 | unique: true, 28 | lowercase: true, 29 | }, 30 | website: { 31 | type: String, 32 | }, 33 | address: { 34 | type: String, 35 | required: [true, "Address is required"], 36 | }, 37 | specialization: { 38 | type: String, 39 | required: [true, "Specialization is required"], 40 | }, 41 | experience: { 42 | type: String, 43 | required: [true, "Experience is required"], 44 | }, 45 | feesPerConsultation: { 46 | type: Number, 47 | required: [true, "Fee is required"], 48 | }, 49 | status: { 50 | type: String, 51 | default: "pending", 52 | }, 53 | starttime: { 54 | type: String, 55 | required: [true], 56 | }, 57 | endtime: { 58 | type: String, 59 | required: [true], 60 | }, 61 | createdAt: { 62 | type: Date, 63 | default: Date.now, 64 | }, 65 | }); 66 | 67 | const docSchema = Joi.object({ 68 | userId: Joi.string(), 69 | firstName: Joi.string().required().min(2).max(50), 70 | lastName: Joi.string().required().min(2).max(50), 71 | phone: Joi.string().required(), 72 | email: Joi.string().required().email(), 73 | website: Joi.string(), 74 | address: Joi.string().required(), 75 | specialization: Joi.string().required(), 76 | experience: Joi.string().required(), 77 | feesPerConsultation: Joi.number().required(), 78 | status: Joi.string().default('pending'), 79 | starttime: Joi.string().required(), 80 | endtime: Joi.string().required(), 81 | createdAt: Joi.date().default(Date.now), 82 | }); 83 | 84 | // Add the Joi validation to the Mongoose schema 85 | doctorSchema.validateDoctor = async function () { 86 | return docSchema.validateAsync(this.toObject()); 87 | }; 88 | 89 | const doctorModel = mongoose.model("doctors", doctorSchema); 90 | module.exports = doctorModel; 91 | -------------------------------------------------------------------------------- /client/src/pages/doctor/DoctorAppointments.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Layout from "./../../components/Layout"; 3 | 4 | import axios from "axios"; 5 | 6 | import { message, Table } from "antd"; 7 | import moment from "moment"; 8 | 9 | const DoctorAppointments = () => { 10 | const [appointments, setAppointments] = useState([]); 11 | 12 | const getAppointments = async () => { 13 | try { 14 | const res = await axios.get("/api/doctor/doctor-appointments", { 15 | headers: { 16 | Authorization: `Bearer ${localStorage.getItem("token")}`, 17 | }, 18 | }); 19 | if (res.data.success) { 20 | setAppointments(res.data.data); 21 | } 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | getAppointments(); 29 | }, []); 30 | 31 | const handleStatus = async (record, status) => { 32 | try { 33 | const res = await axios.post( 34 | "/api/doctor/update-status", 35 | { appointmentsId: record._id, status }, 36 | { 37 | headers: { 38 | Authorization: `Bearer ${localStorage.getItem("token")}`, 39 | }, 40 | } 41 | ); 42 | if (res.data.success) { 43 | message.success(res.data.message); 44 | getAppointments(); 45 | } 46 | } catch (error) { 47 | console.log(error); 48 | message.error("Something Went Wrong"); 49 | } 50 | }; 51 | 52 | const columns = [ 53 | { 54 | title: "ID", 55 | dataIndex: "_id", 56 | }, 57 | { 58 | title: "Date & Time", 59 | dataIndex: "date", 60 | render: (text, record) => ( 61 | 62 | {moment(record.date).format("DD-MM-YYYY")}   63 | {moment(record.time).format("HH:mm")} 64 | 65 | ), 66 | }, 67 | { 68 | title: "Status", 69 | dataIndex: "status", 70 | }, 71 | { 72 | title: "Actions", 73 | dataIndex: "actions", 74 | render: (text, record) => ( 75 |
76 | {record.status === "pending" && ( 77 |
78 | 84 | 90 |
91 | )} 92 |
93 | ), 94 | }, 95 | ]; 96 | return ( 97 | 98 |

Appointments Lists

99 |
100 | 101 | ); 102 | }; 103 | 104 | export default DoctorAppointments; -------------------------------------------------------------------------------- /client/src/pages/Register.js: -------------------------------------------------------------------------------- 1 | import { Form, Input, message } from "antd"; 2 | import axios from "axios"; 3 | import React from "react"; 4 | import { useDispatch } from "react-redux"; 5 | import { Link, useNavigate } from "react-router-dom"; 6 | import { hideLoading, showLoading } from "../redux/features/alertSlice"; 7 | 8 | const Register = () => { 9 | const navigate = useNavigate(); 10 | const dispatch = useDispatch(); 11 | 12 | // Submit for Register 13 | const submitHandler = async (values) => { 14 | try { 15 | dispatch(showLoading()); 16 | const { data } = await axios.post("/api/user/register", values); 17 | dispatch(hideLoading()); 18 | if (data.success) { 19 | message.success("Registration Successful"); 20 | navigate("/login"); 21 | } else { 22 | message.error(data.message); 23 | } 24 | } catch (error) { 25 | dispatch(hideLoading()); 26 | message.error("Registration Failed"); 27 | } 28 | }; 29 | 30 | return ( 31 | <> 32 |
33 |
34 |

Register

35 | 44 | 45 | 46 | 54 | 55 | 56 | \\/?])/, 66 | message: 67 | "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character", 68 | }, 69 | ]} 70 | > 71 | 72 | 73 |
74 | Already Registered? Click Here to Login 75 | 76 |
77 | 78 |
79 | 80 | ); 81 | }; 82 | 83 | export default Register; -------------------------------------------------------------------------------- /client/src/styles/LayoutStyles.css: -------------------------------------------------------------------------------- 1 | /* Light mode styles */ 2 | .main { 3 | padding: 20px; 4 | height: 100vh; 5 | background-color: #fff; 6 | color: #3d3d3d; 7 | } 8 | 9 | .layout { 10 | display: flex; 11 | } 12 | 13 | .sidebar { 14 | min-height: 760px; 15 | height: auto; 16 | width: 300px; 17 | border-radius: 8px; 18 | background-color: #f7f7f7; 19 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 20 | margin-right: 20px; 21 | color: #3d3d3d; 22 | font-size: 1.1rem; 23 | font-weight: 600; 24 | } 25 | 26 | .content { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .header { 32 | height: 60px; 33 | margin-bottom: 20px; 34 | border-radius: 8px; 35 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 36 | background-color: #fff; 37 | display: flex; 38 | align-items: center; 39 | padding: 0 20px; 40 | } 41 | 42 | .body { 43 | height: calc(100% - 80px); 44 | border-radius: 8px; 45 | margin-bottom: 20px; 46 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 47 | background-color: #fff; 48 | overflow-y: auto; 49 | padding: 20px; 50 | } 51 | 52 | .logo h6 { 53 | font-size: 1.5rem; 54 | text-align: center; 55 | margin: 20px 0px; 56 | color: #3d3d3d; 57 | font-weight: 700; 58 | letter-spacing: 2px; 59 | } 60 | 61 | .menu { 62 | margin-top: 50px; 63 | } 64 | 65 | .menu-item { 66 | margin-top: 25px; 67 | display: flex; 68 | align-items: center; 69 | padding: 10px 20px; 70 | border-radius: 8px; 71 | transition: all 0.3s ease; 72 | cursor: pointer; 73 | } 74 | 75 | .menu-item:hover { 76 | background-color: grey; 77 | } 78 | 79 | .menu-item a { 80 | color: #3d3d3d; 81 | text-decoration: none; 82 | margin-left: 20px; 83 | } 84 | 85 | .menu-item i { 86 | font-size: 1.2rem; 87 | margin-right: 20px; 88 | color: #3d3d3d; 89 | } 90 | 91 | .active { 92 | background-color: #252525; 93 | color: #fff; 94 | } 95 | 96 | .active a, 97 | .active i { 98 | color: #fff; 99 | } 100 | 101 | .header-content { 102 | display: flex; 103 | align-items: center; 104 | height: 100%; 105 | justify-content: flex-end; 106 | margin-right: 20px; 107 | } 108 | 109 | .header-content i { 110 | margin-right: 20px; 111 | font-size: 1.2rem; 112 | color: #3d3d3d; 113 | } 114 | 115 | .header-content a { 116 | text-decoration: none; 117 | font-size: 1.1rem; 118 | color: #3d3d3d; 119 | margin: 0 10px; 120 | text-transform: uppercase; 121 | font-weight: 600; 122 | letter-spacing: 1px; 123 | } 124 | 125 | /*****************/ 126 | .date-picker { 127 | width: 90%; 128 | margin: 0 auto; 129 | } 130 | 131 | .time-picker { 132 | width: 90%; 133 | margin: 0 auto; 134 | } 135 | 136 | .d-flex-center { 137 | display: flex; 138 | justify-content: center; 139 | } 140 | .appoint-card-body { 141 | display: flex; 142 | background-color: #f7f7f7; 143 | flex-direction: column; 144 | align-items: center; 145 | justify-content: center; 146 | border-radius: 8px; 147 | } 148 | .card-body { 149 | background-color: #f7f7f7; 150 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 151 | width: auto; 152 | } 153 | .user-doctor-admin-name { 154 | margin-top: 8px; 155 | } 156 | -------------------------------------------------------------------------------- /models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Joi = require('joi'); 3 | const zxcvbn = require('zxcvbn'); 4 | 5 | // Define a schema for user input validation 6 | const userSchema = Joi.object({ 7 | name: Joi.string().min(2).max(50).required(), 8 | email: Joi.string().email().required(), 9 | password: Joi.string() 10 | .pattern(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/) 11 | .min(8) 12 | .max(128) 13 | .required(), 14 | isAdmin: Joi.boolean().optional(), 15 | isDoctor: Joi.boolean().optional(), 16 | notification: Joi.array().items(Joi.object({ 17 | type: Joi.string().required(), 18 | message: Joi.string().required(), 19 | data: Joi.object({ 20 | doctorId: Joi.string(), 21 | name: Joi.string(), 22 | onClickPath: Joi.string(), 23 | }).optional(), 24 | createdAt: Joi.date().required(), 25 | })).optional(), 26 | seennotification: Joi.array().items(Joi.object({ 27 | type: Joi.string().required(), 28 | message: Joi.string().required(), 29 | data: Joi.object({ 30 | doctorId: Joi.string(), 31 | name: Joi.string(), 32 | onClickPath: Joi.string(), 33 | }).optional(), 34 | createdAt: Joi.date().required(), 35 | })).optional(), 36 | }); 37 | 38 | 39 | // Create a Mongoose schema 40 | const userMongooseSchema = new mongoose.Schema({ 41 | name: { 42 | type: String, 43 | required: [true, 'Please provide your name'], 44 | minlength: [2, 'Your name must be at least 2 characters'], 45 | maxlength: [50, 'Your name cannot exceed 50 characters'], 46 | }, 47 | email: { 48 | type: String, 49 | required: [true, 'Please provide your email'], 50 | unique: true, 51 | lowercase: true, 52 | }, 53 | password: { 54 | type: String, 55 | required: [true, 'Please provide a password'], 56 | validate: [ 57 | { 58 | validator: function (value) { 59 | const passwordStrength = zxcvbn(value).score; 60 | return passwordStrength >= 3; // Require a minimum strength of 3 out of 4 61 | }, 62 | message: 'Password is too weak', 63 | }, 64 | { 65 | validator: function (value) { 66 | return /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/.test(value); 67 | }, 68 | message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', 69 | }, 70 | ], 71 | }, 72 | isAdmin: { 73 | type: Boolean, 74 | default: false, 75 | }, 76 | isDoctor: { 77 | type: Boolean, 78 | default: false, 79 | }, 80 | notification: { 81 | type: Array, 82 | default: [], 83 | }, 84 | seennotification: { 85 | type: Array, 86 | default: [], 87 | }, 88 | createdAt: { 89 | type: Date, 90 | default: Date.now, 91 | }, 92 | }); 93 | 94 | // Add the Joi validation to the Mongoose schema 95 | userMongooseSchema.validateUser = async function (user) { 96 | return userSchema.validateAsync(user); 97 | }; 98 | 99 | // Create a Mongoose model 100 | const User = mongoose.model('User', userMongooseSchema); 101 | 102 | // Export the model 103 | module.exports = User; -------------------------------------------------------------------------------- /client/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import { Badge, message } from "antd"; 2 | import React from "react"; 3 | import { useSelector } from "react-redux"; 4 | import { Link, useLocation, useNavigate } from "react-router-dom"; 5 | import "../styles/LayoutStyles.css"; 6 | import { adminMenu, userMenu } from "./../Data/data"; 7 | 8 | const Layout = ({ children }) => { 9 | const { user } = useSelector((state) => state.user); 10 | const location = useLocation(); 11 | const navigate = useNavigate(); 12 | 13 | // logout function 14 | const handleLogout = () => { 15 | localStorage.clear(); 16 | message.success("Logout Successfully"); 17 | navigate("/login"); 18 | }; 19 | // =========== doctor menu =============== 20 | const doctorMenu = [ 21 | { 22 | name: "Home", 23 | path: "/", 24 | icon: "fa-solid fa-house", 25 | }, 26 | { 27 | name: "Appointments", 28 | path: "/doctor-appointments", 29 | icon: "fa-solid fa-list", 30 | }, 31 | 32 | { 33 | name: "Profile", 34 | path: `/doctor/profile/${user?._id}`, 35 | icon: "fa-solid fa-user", 36 | }, 37 | ]; 38 | // =========== doctor menu =============== 39 | 40 | // rendering menu list 41 | const SidebarMenu = user?.isAdmin 42 | ? adminMenu.filter((menu) => menu.name !== "Profile") 43 | : user?.isDoctor 44 | ? doctorMenu 45 | : userMenu.filter((menu) => menu.name !== "Profile"); 46 | 47 | return ( 48 | <> 49 |
50 |
51 |
52 |
53 |
AppointDoc
54 |
55 |
56 |
57 | {SidebarMenu.map((menu) => { 58 | const isActive = location.pathname === menu.path; 59 | return ( 60 |
64 | 65 | {menu.name} 66 |
67 | ); 68 | })} 69 |
70 | 71 | Logout 72 |
73 |
74 |
75 |
76 |
77 |
78 | { 83 | navigate("/notification"); 84 | }} 85 | > 86 | 87 | 88 |
89 |
90 |
{user?.name}
91 |
92 |
93 |
{children}
94 |
95 |
96 |
97 | 98 | ); 99 | }; 100 | 101 | export default Layout; 102 | -------------------------------------------------------------------------------- /client/build/static/js/main.63fc4e3b.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2018 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | 7 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 8 | 9 | /** 10 | * @license React 11 | * react-dom.production.min.js 12 | * 13 | * Copyright (c) Facebook, Inc. and its affiliates. 14 | * 15 | * This source code is licensed under the MIT license found in the 16 | * LICENSE file in the root directory of this source tree. 17 | */ 18 | 19 | /** 20 | * @license React 21 | * react-is.production.min.js 22 | * 23 | * Copyright (c) Facebook, Inc. and its affiliates. 24 | * 25 | * This source code is licensed under the MIT license found in the 26 | * LICENSE file in the root directory of this source tree. 27 | */ 28 | 29 | /** 30 | * @license React 31 | * react-jsx-runtime.production.min.js 32 | * 33 | * Copyright (c) Facebook, Inc. and its affiliates. 34 | * 35 | * This source code is licensed under the MIT license found in the 36 | * LICENSE file in the root directory of this source tree. 37 | */ 38 | 39 | /** 40 | * @license React 41 | * react.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** 50 | * @license React 51 | * scheduler.production.min.js 52 | * 53 | * Copyright (c) Facebook, Inc. and its affiliates. 54 | * 55 | * This source code is licensed under the MIT license found in the 56 | * LICENSE file in the root directory of this source tree. 57 | */ 58 | 59 | /** 60 | * @license React 61 | * use-sync-external-store-shim.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** 70 | * @license React 71 | * use-sync-external-store-shim/with-selector.production.min.js 72 | * 73 | * Copyright (c) Facebook, Inc. and its affiliates. 74 | * 75 | * This source code is licensed under the MIT license found in the 76 | * LICENSE file in the root directory of this source tree. 77 | */ 78 | 79 | /** 80 | * @remix-run/router v1.6.0 81 | * 82 | * Copyright (c) Remix Software Inc. 83 | * 84 | * This source code is licensed under the MIT license found in the 85 | * LICENSE.md file in the root directory of this source tree. 86 | * 87 | * @license MIT 88 | */ 89 | 90 | /** 91 | * React Router DOM v6.11.0 92 | * 93 | * Copyright (c) Remix Software Inc. 94 | * 95 | * This source code is licensed under the MIT license found in the 96 | * LICENSE.md file in the root directory of this source tree. 97 | * 98 | * @license MIT 99 | */ 100 | 101 | /** 102 | * React Router v6.11.0 103 | * 104 | * Copyright (c) Remix Software Inc. 105 | * 106 | * This source code is licensed under the MIT license found in the 107 | * LICENSE.md file in the root directory of this source tree. 108 | * 109 | * @license MIT 110 | */ 111 | 112 | /** @license React v16.13.1 113 | * react-is.production.min.js 114 | * 115 | * Copyright (c) Facebook, Inc. and its affiliates. 116 | * 117 | * This source code is licensed under the MIT license found in the 118 | * LICENSE file in the root directory of this source tree. 119 | */ 120 | 121 | //! moment.js 122 | -------------------------------------------------------------------------------- /controllers/doctorController.js: -------------------------------------------------------------------------------- 1 | const appointmentModel = require("../models/appointmentModel"); 2 | const doctorModel = require("../models/doctorModel"); 3 | const userModel = require("../models/userModel"); 4 | const getDoctorInfoController = async (req, res) => { 5 | try { 6 | const doctor = await doctorModel.findOne({ userId: req.body.userId }); 7 | res.status(200).send({ 8 | success: true, 9 | message: "Doctor Data Fetch Success", 10 | data: doctor, 11 | }); 12 | } catch (error) { 13 | console.log(error); 14 | res.status(500).send({ 15 | success: false, 16 | error, 17 | message: "Error in Fetching Doctor Details", 18 | }); 19 | } 20 | }; 21 | 22 | // update doc profile 23 | const updateProfileController = async (req, res) => { 24 | try { 25 | const doctor = await doctorModel.findOneAndUpdate( 26 | { userId: req.body.userId }, 27 | req.body 28 | ); 29 | res.status(201).send({ 30 | success: true, 31 | message: "Doctor Profile Updated", 32 | data: doctor, 33 | }); 34 | } catch (error) { 35 | console.log(error); 36 | res.status(500).send({ 37 | success: false, 38 | message: "Doctor Profile Update Issue", 39 | error, 40 | }); 41 | } 42 | }; 43 | 44 | //get single docotor 45 | const getDoctorByIdController = async (req, res) => { 46 | try { 47 | const doctor = await doctorModel.findOne({ _id: req.body.doctorId }); 48 | res.status(200).send({ 49 | success: true, 50 | message: "Single Doctor Info Fetched", 51 | data: doctor, 52 | }); 53 | } catch (error) { 54 | console.log(error); 55 | res.status(500).send({ 56 | success: false, 57 | error, 58 | message: "Error in Single Doctor Info", 59 | }); 60 | } 61 | }; 62 | 63 | const doctorAppointmentsController = async (req, res) => { 64 | try { 65 | const doctor = await doctorModel.findOne({ userId: req.body.userId }); 66 | const appointments = await appointmentModel.find({ 67 | doctorId: doctor._id, 68 | }); 69 | res.status(200).send({ 70 | success: true, 71 | message: "Doctor Appointments Fetch Successfully", 72 | data: appointments, 73 | }); 74 | } catch (error) { 75 | console.log(error); 76 | res.status(500).send({ 77 | success: false, 78 | error, 79 | message: "Error In Doctor Appointments", 80 | }); 81 | } 82 | }; 83 | 84 | const updateStatusController = async (req, res) => { 85 | try { 86 | const { appointmentsId, status } = req.body; 87 | const appointments = await appointmentModel.findByIdAndUpdate( 88 | appointmentsId, 89 | { status } 90 | ); 91 | const user = await userModel.findOne({ _id: appointments.userId }); 92 | let notification = user.notification || []; // Initialize notifcation to an empty array if it's undefined 93 | notification.push({ 94 | type: "status-updated", 95 | message: `Your Appointment Has Been Updated ${status}`, 96 | onCLickPath: "/doctor-appointments", 97 | }); 98 | await userModel.updateOne( 99 | { _id: user._id }, 100 | { $set: { notification: notification } } 101 | ); 102 | res.status(200).send({ 103 | success: true, 104 | message: "Appointment Status Updated", 105 | }); 106 | } catch (error) { 107 | console.log(error); 108 | res.status(500).send({ 109 | success: false, 110 | error, 111 | message: "Error In Update Status", 112 | }); 113 | } 114 | }; 115 | 116 | 117 | module.exports = { getDoctorInfoController, updateProfileController, getDoctorByIdController, doctorAppointmentsController, updateStatusController }; -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 3 | import ProtectedRoute from "./components/ProtectedRoute"; 4 | import PublicRoute from "./components/PublicRoute"; 5 | import Spinner from "./components/Spinner"; 6 | import ApplyDoctor from "./pages/ApplyDoctor"; 7 | import Appointments from "./pages/Appointments"; 8 | import BookingPage from "./pages/BookingPage"; 9 | import HomePage from "./pages/HomePage"; 10 | import Login from "./pages/Login"; 11 | import NotificationPage from "./pages/NotificationPage"; 12 | import Register from "./pages/Register"; 13 | import Doctors from "./pages/admin/Doctors"; 14 | import Users from "./pages/admin/Users"; 15 | import DoctorAppointments from "./pages/doctor/DoctorAppointments"; 16 | import Profile from "./pages/doctor/Profile"; 17 | 18 | function App() { 19 | const { loading } = useSelector((state) => state.alerts); 20 | return ( 21 | <> 22 | 23 | {loading ? ( 24 | 25 | ) : ( 26 | 27 | 31 | 32 | 33 | } 34 | /> 35 | 39 | 40 | 41 | } 42 | /> 43 | 47 | 48 | 49 | } 50 | /> 51 | 55 | 56 | 57 | } 58 | /> 59 | 63 | 64 | 65 | } 66 | /> 67 | 71 | 72 | 73 | } 74 | /> 75 | 79 | 80 | 81 | } 82 | /> 83 | 87 | 88 | 89 | } 90 | /> 91 | 95 | 96 | 97 | } 98 | /> 99 | 103 | 104 | 105 | } 106 | /> 107 | 111 | 112 | 113 | } 114 | /> 115 | 116 | )} 117 | 118 | 119 | ); 120 | } 121 | 122 | export default App; 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppointDoc 2 | 3 | This is a professional-grade MERN (MongoDB, Express, React, Node.js) stack web application for booking doctor appointments. Patients can browse through a list of doctors and their availability and book an appointment with their preferred doctor. Doctors can view their schedule, manage their availability, and approve/cancel appointments. 4 | 5 | ## Project View 6 | 7 |

1. For User Profile

8 | 9 | - **Homepage** 10 |

11 | img-name 12 |

13 | 14 | - **Appointment Lists** 15 |

16 | img-name 17 |

18 | 19 | - **Booking Appointment** 20 |

21 | img-name 22 |

23 | 24 | - **Apply As Doctor** 25 |

26 | img-name 27 |

28 | 29 | - **New Notifications** 30 |

31 | img-name 32 |

33 | 34 | - **Read Notifications** 35 |

36 | img-name 37 |

38 | 39 |

2. For Doctor Profile

40 | 41 | - **Homepage** 42 |

43 | img-name 44 |

45 | 46 | - **Appointment Lists** 47 |

48 | img-name 49 |

50 | 51 | - **Manage Profile** 52 |

53 | img-name 54 |

55 | 56 |

3. For Admin Profile

57 | 58 | - **Homepage** 59 |

60 | img-name 61 |

62 | 63 | - **Doctors List** 64 |

65 | img-name 66 |

67 | 68 | - **Users List** 69 |

70 | img-name 71 |

72 | 73 | ## Installation 74 | 75 | To set up BloodLife locally, follow these steps: 76 | 77 | - Clone the repository: 78 | 79 | git clone https://github.com/OviSarkar62/AppointDoc.git 80 | 81 | - Install the required dependencies for backend: 82 | 83 | npm install express joi jsonwebtoken moment mongoose morgan nodemon zxcvbn dotenv colors bcryptjs 84 | 85 | - Navigate to the client directory: 86 | 87 | cd client 88 | 89 | - Install the dependencies for the client: 90 | 91 | npm i react-router-dom react-redux axios antd @reduxjs/toolkit react-bootstrap moment 92 | 93 | - Create a .env file in the root directory with the following environment variables: 94 | 95 | DB_URL = mongodb+srv://:@cluster0.l17quyr.mongodb.net/database 96 | 97 | JWT_SECRET = A_Secret_Value 98 | 99 | PORT = 4000 100 | 101 | - Start the server: 102 | 103 | npm start 104 | 105 | - In a new terminal window, navigate to the client directory: 106 | 107 | cd client 108 | 109 | - Start the client: 110 | 111 | npm start 112 | 113 | - Access the application. Open your web browser and visit http://localhost:3000 to access the application. 114 | 115 | 116 | ## Usage 117 | 118 | - The AppointDoc application allows doctors to manage their appointments with ease. Doctors can create new appointments, view existing appointments, and approve or reject appointments as necessary. 119 | 120 | - To create a new appointment, the user needs to click on the "New Appointment" button on the doctor's card. They will then be presented with a form where they can enter the details of the appointment, including date and time. 121 | 122 | - To view existing appointments, the doctor needs to click on the "Appointments" button on the sidebar. They will then be presented with a list of all their appointments, presented by date and time. The doctor can click on any appointment to approve or reject to change it's pending status. 123 | 124 | - To update time of availability, the doctor needs to click on the profile page where they wish to update the time of appointments. They will then be presented with a form where they can edit the start time and end time details of the appointment. 125 | 126 | ## Stack 127 | 128 | - MongoDB - NoSQL database for storing data 129 | - Express - Backend framework for building RESTful APIs 130 | - React - Frontend framework for building user interfaces 131 | - Node.js - JavaScript runtime environment for building scalable server-side applications 132 | - JWT - JSON Web Token for user authentication and authorization 133 | - Bcrypt - Password hashing library for secure password storage 134 | 135 | ## Live Link 136 | 137 | The live project: [AppointDoc](https://appoint-doc.vercel.app/login) 138 | -------------------------------------------------------------------------------- /client/build/static/css/main.9cc7b55e.css: -------------------------------------------------------------------------------- 1 | body,html{height:100%;width:100%}input::-ms-clear,input::-ms-reveal{display:none}*,:after,:before{box-sizing:border-box}html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:sans-serif;line-height:1.15}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:initial;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{font-weight:500;margin-bottom:.5em;margin-top:0}p{margin-bottom:1em;margin-top:0}abbr[data-original-title],abbr[title]{border-bottom:0;cursor:help;-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted}address{font-style:normal;line-height:inherit;margin-bottom:1em}input[type=number],input[type=password],input[type=text],textarea{-webkit-appearance:none}dl,ol,ul{margin-bottom:1em;margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}code,kbd,pre,samp{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:1em}pre{margin-bottom:1em;margin-top:0;overflow:auto}figure{margin:0 0 1em}img{border-style:none;vertical-align:middle}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{caption-side:bottom;padding-bottom:.3em;padding-top:.75em;text-align:left}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{color:inherit;display:block;font-size:1.5em;line-height:inherit;margin-bottom:.5em;max-width:100%;padding:0;white-space:normal;width:100%}progress{vertical-align:initial}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:none;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{background-color:#feffe6;padding:.2em}.main{background-color:#fff;color:#3d3d3d;height:100vh;padding:20px}.layout{display:flex}.sidebar{background-color:#f7f7f7;border-radius:8px;box-shadow:0 0 2px rgba(0,0,0,.1);color:#3d3d3d;font-size:1.1rem;font-weight:600;height:auto;margin-right:20px;min-height:760px;width:300px}.content{height:100%;width:100%}.header{align-items:center;display:flex;height:60px;padding:0 20px}.body,.header{background-color:#fff;border-radius:8px;box-shadow:0 0 2px rgba(0,0,0,.1);margin-bottom:20px}.body{height:calc(100% - 80px);overflow-y:auto;padding:20px}.logo h6{color:#3d3d3d;font-size:1.5rem;font-weight:700;letter-spacing:2px;margin:20px 0;text-align:center}.menu{margin-top:50px}.menu-item{align-items:center;border-radius:8px;cursor:pointer;display:flex;margin-top:25px;padding:10px 20px;transition:all .3s ease}.menu-item:hover{background-color:grey}.menu-item a{color:#3d3d3d;margin-left:20px;text-decoration:none}.menu-item i{color:#3d3d3d;font-size:1.2rem;margin-right:20px}.active{background-color:#252525}.active,.active a,.active i{color:#fff}.header-content{align-items:center;display:flex;height:100%;justify-content:flex-end;margin-right:20px}.header-content i{color:#3d3d3d;font-size:1.2rem;margin-right:20px}.header-content a{color:#3d3d3d;font-size:1.1rem;font-weight:600;letter-spacing:1px;margin:0 10px;text-decoration:none;text-transform:uppercase}.date-picker,.time-picker{margin:0 auto;width:90%}.appoint-card-body,.d-flex-center{display:flex;justify-content:center}.appoint-card-body{align-items:center;background-color:#f7f7f7;border-radius:8px;flex-direction:column}.card-body{background-color:#f7f7f7;box-shadow:0 0 2px rgba(0,0,0,.1);width:auto}.user-doctor-admin-name{margin-top:8px}.spinner{left:50%;position:absolute;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:100}.spinner-border{height:50px;width:50px}.register-page{align-items:center;display:flex;height:90vh;justify-content:center}.register-page form{background-color:#fff;border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,.2);padding:40px;width:400px}.register-page form .d-flex{align-items:center;display:flex;justify-content:space-between;margin-top:20px}.register-page form .d-flex a{color:#007bff;text-decoration:none}.register-page h1{text-align:center}.register-page .ant-form-item,.register-page h1{margin-bottom:20px}.register-page .ant-form-item-label{font-weight:700}.register-page .ant-form-item-explain{color:#ff4d4f}.register-page .ant-form-item-has-error .ant-input{border-color:#ff4d4f}.register-page .ant-form-item-has-error .ant-input:focus{box-shadow:0 0 0 2px rgba(255,77,79,.2)}.register-page .btn-primary{width:100%} 2 | /*# sourceMappingURL=main.9cc7b55e.css.map*/ -------------------------------------------------------------------------------- /client/src/pages/BookingPage.js: -------------------------------------------------------------------------------- 1 | import { DatePicker, TimePicker, message } from "antd"; 2 | import axios from "axios"; 3 | import React, { useEffect, useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { useParams } from "react-router-dom"; 6 | import Layout from "../components/Layout"; 7 | import { hideLoading, showLoading } from "../redux/features/alertSlice"; 8 | import "./../styles/LayoutStyles.css"; 9 | 10 | const BookingPage = () => { 11 | const { user } = useSelector((state) => state.user); 12 | const params = useParams(); 13 | const [doctors, setDoctors] = useState([]); 14 | const [date, setDate] = useState(""); 15 | const [time, setTime] = useState(""); 16 | const [isAvailable, setIsAvailable] = useState(); 17 | const dispatch = useDispatch(); 18 | // login user data 19 | const getUserData = async () => { 20 | try { 21 | const res = await axios.post( 22 | "/api/doctor/getDoctorById", 23 | { doctorId: params.doctorId }, 24 | { 25 | headers: { 26 | Authorization: "Bearer " + localStorage.getItem("token"), 27 | }, 28 | } 29 | ); 30 | if (res.data.success) { 31 | setDoctors(res.data.data); 32 | } 33 | } catch (error) { 34 | console.log(error); 35 | } 36 | }; 37 | 38 | // handleAvailability function 39 | const handleAvailability = async () => { 40 | try { 41 | dispatch(showLoading()); 42 | const res = await axios.post( 43 | "/api/user/booking-availbility", 44 | { 45 | doctorId: params.doctorId, 46 | date, 47 | time, 48 | }, 49 | { 50 | headers: { 51 | Authorization: `Bearer ${localStorage.getItem("token")}`, 52 | }, 53 | } 54 | ); 55 | dispatch(hideLoading()); 56 | if (res.data.success) { 57 | setIsAvailable(true); 58 | console.log(isAvailable); 59 | message.success(res.data.message); 60 | } else { 61 | message.error(res.data.message); 62 | } 63 | } catch (error) { 64 | dispatch(hideLoading()); 65 | console.log(error); 66 | } 67 | }; 68 | 69 | // =============== booking func 70 | const handleBooking = async () => { 71 | try { 72 | setIsAvailable(true); 73 | if (!date && !time) { 74 | return alert("Date & Time Required"); 75 | } 76 | dispatch(showLoading()); 77 | const res = await axios.post( 78 | "/api/user/book-appointment", 79 | { 80 | doctorId: params.doctorId, 81 | userId: user._id, 82 | doctorInfo: doctors, 83 | userInfo: user, 84 | date: date, 85 | time: time, 86 | }, 87 | { 88 | headers: { 89 | Authorization: `Bearer ${localStorage.getItem("token")}`, 90 | }, 91 | } 92 | ); 93 | dispatch(hideLoading()); 94 | if (res.data.success) { 95 | message.success(res.data.message); 96 | } else { 97 | message.error(res.data.message); 98 | } 99 | } catch (error) { 100 | dispatch(hideLoading()); 101 | console.log(error); 102 | } 103 | }; 104 | 105 | 106 | useEffect(() => { 107 | getUserData(); 108 | //eslint-disable-next-line 109 | }, []); 110 | 111 | return ( 112 | 113 |
114 |

Book an Appointment

115 | {doctors && ( 116 |
117 |
118 |
119 | Dr. {doctors.firstName} {doctors.lastName} 120 |
121 |
122 | Fees: {doctors.feesPerConsultation} 123 |
124 |
125 | Timings: {doctors.starttime} - {doctors.endtime} 126 |
127 |
128 |
129 | { 133 | const selectedDate = value 134 | ? value.format("DD-MM-YYYY") 135 | : ""; 136 | setDate(selectedDate); 137 | }} 138 | /> 139 | setTime(time && time.format("HH:mm"))} 143 | /> 144 |
145 | 151 |
152 |
153 | 159 |
160 |
161 |
162 |
163 |
164 | )} 165 |
166 |
167 | ); 168 | }; 169 | 170 | export default BookingPage; 171 | -------------------------------------------------------------------------------- /client/src/pages/NotificationPage.js: -------------------------------------------------------------------------------- 1 | import { message, Tabs } from "antd"; 2 | import axios from "axios"; 3 | import React, { useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { 7 | hideLoading, 8 | showLoading 9 | } from "../redux/features/alertSlice"; 10 | import { setUser } from "../redux/features/userSlice"; 11 | import Layout from "./../components/Layout"; 12 | 13 | const NotificationPage = () => { 14 | const dispatch = useDispatch(); 15 | const navigate = useNavigate(); 16 | const { user } = useSelector((state) => state.user); 17 | const [refreshNotifications, setRefreshNotifications] = useState(false); 18 | 19 | 20 | // handle read notification 21 | const handleMarkAllRead = async () => { 22 | try { 23 | dispatch(showLoading()); 24 | const res = await axios.post( 25 | "/api/user/get-all-notification", 26 | { 27 | userId: user._id, 28 | }, 29 | { 30 | headers: { 31 | Authorization: `Bearer ${localStorage.getItem("token")}`, 32 | }, 33 | } 34 | ); 35 | dispatch(hideLoading()); 36 | if (res.data.success) { 37 | const unreadNotifications = res.data.notifications 38 | ? res.data.notifications.filter( 39 | (notification) => !notification.isRead 40 | ) 41 | : []; 42 | updateNotificationsInStore(unreadNotifications); 43 | setRefreshNotifications(!refreshNotifications); 44 | window.location.reload(); 45 | message.success(res.data.message); 46 | } else { 47 | message.error(res.data.message); 48 | } 49 | } catch (error) { 50 | dispatch(hideLoading()); 51 | console.log(error); 52 | message.error("Something Went Wrong"); 53 | } 54 | }; 55 | 56 | 57 | // handle delete notification 58 | const handleDeleteAllRead = async () => { 59 | try { 60 | dispatch(showLoading()); 61 | const res = await axios.post( 62 | "/api/user/delete-all-notification", 63 | { 64 | userId: user._id, 65 | }, 66 | { 67 | headers: { 68 | Authorization: `Bearer ${localStorage.getItem("token")}`, 69 | }, 70 | } 71 | ); 72 | dispatch(hideLoading()); 73 | if (res.data.success) { 74 | const updatedNotifications = res.data.notifications || []; // Add a default value of an empty array if the notifications array is undefined 75 | const updatedUser = { 76 | ...user, 77 | notification: updatedNotifications.filter(notification => !notification.isRead), 78 | seennotification: updatedNotifications.filter(notification => notification.isRead), 79 | }; 80 | dispatch(setUser(updatedUser)); 81 | setRefreshNotifications(!refreshNotifications); 82 | message.success(res.data.message); 83 | } else { 84 | message.error(res.data.message); 85 | } 86 | } catch (error) { 87 | dispatch(hideLoading()); 88 | console.log(error); 89 | message.error("Something Went Wrong"); 90 | } 91 | }; 92 | 93 | 94 | const updateNotificationsInStore = (notifications) => { 95 | const updatedUser = { 96 | ...user, 97 | notification: [], 98 | seennotification: [], 99 | }; 100 | notifications.forEach((notification) => { 101 | if (notification.isRead) { 102 | updatedUser.seennotification.push(notification); 103 | } else { 104 | updatedUser.notification.push(notification); 105 | } 106 | }); 107 | dispatch(setUser(updatedUser)); 108 | }; 109 | 110 | 111 | return ( 112 | 113 |

Notifications

114 | 115 | 116 |
117 | 124 |
125 | {user && user.notification && user.notification.length > 0 ? ( 126 | user.notification.map((notificationMgs) => ( 127 |
132 |
{ 135 | navigate(notificationMgs.onClickPath); 136 | }} 137 | > 138 | {notificationMgs.message} 139 |
140 |
141 | )) 142 | ) : ( 143 |

You have no new notifications.

144 | )} 145 |
146 | 147 |
148 | 155 |
156 | {user && user.seennotification && user.seennotification.length > 0 ? ( 157 | user.seennotification.map((notificationMgs) => ( 158 |
163 |
navigate(notificationMgs.onClickPath)} 166 | > 167 | {notificationMgs.message} 168 |
169 |
170 | )) 171 | ) : ( 172 |

You have no read notifications

173 | )} 174 |
175 |
176 |
177 | ); 178 | }; 179 | 180 | export default NotificationPage; 181 | -------------------------------------------------------------------------------- /client/src/pages/ApplyDoctor.js: -------------------------------------------------------------------------------- 1 | import { Col, Form, Input, Row, TimePicker, message } from "antd"; 2 | import axios from "axios"; 3 | import React from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { hideLoading, showLoading } from "../redux/features/alertSlice"; 7 | import Layout from "./../components/Layout"; 8 | 9 | const ApplyDoctor = () => { 10 | const { user } = useSelector((state) => state.user); 11 | 12 | const dispatch = useDispatch(); 13 | const navigate = useNavigate(); 14 | 15 | const handleFinish = async (values) => { 16 | try { 17 | dispatch(showLoading()); 18 | const starttime = values.starttime.format("HH:mm"); 19 | const endtime = values.endtime.format("HH:mm"); 20 | const res = await axios.post( 21 | "/api/user/apply-doctor", 22 | { 23 | ...values, 24 | userId: user._id, 25 | starttime, 26 | endtime, 27 | }, 28 | { 29 | headers: { 30 | Authorization: `Bearer ${localStorage.getItem("token")}`, 31 | }, 32 | } 33 | ); 34 | dispatch(hideLoading()); 35 | if (res.data.success) { 36 | message.success(res.data.message); 37 | navigate("/"); 38 | } else { 39 | message.error(res.data.message); 40 | } 41 | } catch (error) { 42 | dispatch(hideLoading()); 43 | console.log(error); 44 | message.error("Something Went Wrong"); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 |

Apply Doctor

51 |
52 |

Personal Details :

53 | 54 |

55 | 61 | 62 | 63 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 106 | 107 | 108 | 109 | 110 |
111 |

Professional Details :

112 | 113 | 114 | 122 | 123 | 124 | 125 | 126 | 132 | 133 | 134 | 135 | 136 | 142 | 143 | 144 | 145 | 146 | 151 | 152 | 153 | 154 | 155 | 160 | 161 | 162 | 163 | 164 | 165 |
166 | 169 | 170 | 171 | 172 | 173 | ); 174 | }; 175 | 176 | export default ApplyDoctor; 177 | -------------------------------------------------------------------------------- /client/src/pages/doctor/Profile.js: -------------------------------------------------------------------------------- 1 | import { Col, Form, Input, Row, TimePicker, message } from "antd"; 2 | import axios from "axios"; 3 | import moment from "moment/moment"; 4 | import React, { useEffect, useState } from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { useNavigate, useParams } from "react-router-dom"; 7 | import { hideLoading, showLoading } from "../../redux/features/alertSlice"; 8 | import Layout from "./../../components/Layout"; 9 | 10 | 11 | const Profile = () => { 12 | const { user } = useSelector((state) => state.user); 13 | const [doctor, setDoctor] = useState(null); 14 | const dispatch = useDispatch(); 15 | const navigate = useNavigate(); 16 | const params = useParams(); 17 | // update doc ========== 18 | //handle form 19 | const handleFinish = async (values) => { 20 | try { 21 | dispatch(showLoading()); 22 | const starttime = values.starttime.format("HH:mm"); 23 | const endtime = values.endtime.format("HH:mm"); 24 | const res = await axios.post( 25 | "/api/doctor/updateProfile", 26 | { 27 | ...values, 28 | userId: user._id, 29 | starttime, 30 | endtime, 31 | }, 32 | { 33 | headers: { 34 | Authorization: `Bearer ${localStorage.getItem("token")}`, 35 | }, 36 | } 37 | ); 38 | dispatch(hideLoading()); 39 | if (res.data.success) { 40 | message.success(res.data.message); 41 | navigate("/"); 42 | } else { 43 | message.error(res.data.message); 44 | } 45 | } catch (error) { 46 | dispatch(hideLoading()); 47 | console.log(error); 48 | message.error("Somthing Went Wrong "); 49 | } 50 | }; 51 | // update doc ========== 52 | 53 | //getDOc Details 54 | const getDoctorInfo = async () => { 55 | try { 56 | const res = await axios.post( 57 | "/api/doctor/getDoctorInfo", 58 | { userId: params.id }, 59 | { 60 | headers: { 61 | Authorization: `Bearer ${localStorage.getItem("token")}`, 62 | }, 63 | } 64 | ); 65 | if (res.data.success) { 66 | setDoctor(res.data.data); 67 | } 68 | } catch (error) { 69 | console.log(error); 70 | } 71 | }; 72 | 73 | useEffect(() => { 74 | getDoctorInfo(); 75 | //eslint-disable-next-line 76 | }, []); 77 | return ( 78 | 79 |

Manage Profile

80 | {doctor && ( 81 |
91 |

Personal Details :

92 | 93 |
94 | 100 | 101 | 102 | 103 | 104 | 110 | 111 | 112 | 113 | 114 | 120 | 121 | 122 | 123 | 124 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 145 | 146 | 147 | 148 | 149 |

Professional Details :

150 | 151 | 152 | 158 | 159 | 160 | 161 | 162 | 168 | 169 | 170 | 171 | 172 | 178 | 179 | 180 | 181 | 182 | 187 | 188 | 189 | 190 | 191 | 196 | 197 | 198 | 199 | 200 | 201 | 204 | 205 | 206 | 207 | )} 208 | 209 | ); 210 | }; 211 | 212 | export default Profile; 213 | -------------------------------------------------------------------------------- /client/build/static/css/main.9cc7b55e.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.9cc7b55e.css","mappings":"AACA,UAGE,WAAY,CADZ,UAEF,CACA,mCAEE,YACF,CACA,iBAGE,qBACF,CACA,KAGE,6BAA8B,CAC9B,yBAA0B,CAC1B,4BAA6B,CAC7B,yCAA6C,CAL7C,sBAAuB,CACvB,gBAKF,CAIA,KACE,QACF,CACA,sBACE,YACF,CACA,GACE,kBAAuB,CACvB,QAAS,CACT,gBACF,CACA,kBAQE,eAAgB,CADhB,kBAAoB,CADpB,YAGF,CACA,EAEE,iBAAkB,CADlB,YAEF,CACA,sCAKE,eAAgB,CAChB,WAAY,CAJZ,wCAAyC,CACzC,yBAA0B,CAC1B,gCAGF,CACA,QAEE,iBAAkB,CAClB,mBAAoB,CAFpB,iBAGF,CACA,kEAIE,uBACF,CACA,SAIE,iBAAkB,CADlB,YAEF,CACA,wBAIE,eACF,CACA,GACE,eACF,CACA,GACE,kBAAoB,CACpB,aACF,CACA,WACE,cACF,CACA,IACE,iBACF,CACA,SAEE,kBACF,CACA,MACE,aACF,CACA,QAGE,aAAc,CACd,aAAc,CAFd,iBAAkB,CAGlB,sBACF,CACA,IACE,aACF,CACA,IACE,SACF,CACA,kBAKE,2EAAqF,CADrF,aAEF,CACA,IAEE,iBAAkB,CADlB,YAAa,CAEb,aACF,CACA,OACE,cACF,CACA,IAEE,iBAAkB,CADlB,qBAEF,CACA,kFASE,yBACF,CACA,MACE,wBACF,CACA,QAIE,mBAAoB,CAFpB,mBAAqB,CADrB,iBAAmB,CAEnB,eAEF,CACA,sCAME,aAAc,CAEd,mBAAoB,CADpB,iBAAkB,CAElB,mBAAoB,CAJpB,QAKF,CACA,aAEE,gBACF,CACA,cAEE,mBACF,CACA,qDAIE,yBACF,CACA,wHAKE,iBAAkB,CADlB,SAEF,CACA,uCAEE,qBAAsB,CACtB,SACF,CACA,+EAIE,0BACF,CACA,SACE,aAAc,CACd,eACF,CACA,SAIE,QAAS,CAFT,QAAS,CADT,WAAY,CAEZ,SAEF,CACA,OAME,aAAc,CALd,aAAc,CAMd,eAAgB,CAChB,mBAAoB,CAJpB,kBAAoB,CADpB,cAAe,CAEf,SAAU,CAIV,kBAAmB,CAPnB,UAQF,CACA,SACE,sBACF,CACA,kFAEE,WACF,CACA,cAEE,uBAAwB,CADxB,mBAEF,CACA,qFAEE,uBACF,CACA,6BAEE,yBAA0B,CAD1B,YAEF,CACA,OACE,oBACF,CACA,QACE,iBACF,CACA,SACE,YACF,CACA,SACE,sBACF,CACA,KAEE,wBAAyB,CADzB,YAEF,CC3PA,MAGE,qBAAsB,CACtB,aAAc,CAFd,YAAa,CADb,YAIF,CAEA,QACE,YACF,CAEA,SAKE,wBAAyB,CADzB,iBAAkB,CAElB,iCAAsC,CAEtC,aAAc,CACd,gBAAiB,CACjB,eAAgB,CARhB,WAAY,CAKZ,iBAAkB,CANlB,gBAAiB,CAEjB,WAQF,CAEA,SAEE,WAAY,CADZ,UAEF,CAEA,QAOE,kBAAmB,CADnB,YAAa,CALb,WAAY,CAOZ,cACF,CAEA,cANE,qBAAsB,CAFtB,iBAAkB,CAClB,iCAAsC,CAFtC,kBAiBF,CARA,MACE,wBAAyB,CAKzB,eAAgB,CAChB,YACF,CAEA,SAIE,aAAc,CAHd,gBAAiB,CAIjB,eAAgB,CAChB,kBAAmB,CAHnB,aAAgB,CADhB,iBAKF,CAEA,MACE,eACF,CAEA,WAGE,kBAAmB,CAEnB,iBAAkB,CAElB,cAAe,CALf,YAAa,CADb,eAAgB,CAGhB,iBAAkB,CAElB,uBAEF,CAEA,iBACE,qBACF,CAEA,aACE,aAAc,CAEd,gBAAiB,CADjB,oBAEF,CAEA,aAGE,aAAc,CAFd,gBAAiB,CACjB,iBAEF,CAEA,QACE,wBAEF,CAEA,4BAHE,UAMF,CAEA,gBAEE,kBAAmB,CADnB,YAAa,CAEb,WAAY,CACZ,wBAAyB,CACzB,iBACF,CAEA,kBAGE,aAAc,CADd,gBAAiB,CADjB,iBAGF,CAEA,kBAGE,aAAc,CADd,gBAAiB,CAIjB,eAAgB,CAChB,kBAAmB,CAHnB,aAAc,CAHd,oBAAqB,CAIrB,wBAGF,CAQA,0BAEE,aAAc,CADd,SAEF,CAMA,kCAHE,YAAa,CACb,sBASF,CAPA,mBAIE,kBAAmB,CAFnB,wBAAyB,CAIzB,iBAAkB,CAHlB,qBAIF,CACA,WACE,wBAAyB,CACzB,iCAAsC,CACtC,UACF,CACA,wBACE,cACF,CC1JA,SAGE,QAAS,CAFT,iBAAkB,CAClB,OAAQ,CAER,sCAAgC,CAAhC,8BAAgC,CAChC,WACF,CAEA,gBACE,WAAY,CACZ,UACF,CAIA,eAEE,kBAAmB,CADnB,YAAa,CAGb,WAAY,CADZ,sBAEF,CAEA,oBAEE,qBAAsB,CAEtB,kBAAmB,CACnB,kCAAuC,CAFvC,YAAa,CAFb,WAKF,CACA,4BAGE,kBAAmB,CAFnB,YAAa,CACb,6BAA8B,CAE9B,eACF,CAEA,8BACE,aAAc,CACd,oBACF,CAEA,kBACE,iBAEF,CAEA,gDAHE,kBAKF,CAEA,oCACE,eACF,CAEA,sCACE,aACF,CAEA,mDACE,oBACF,CAEA,yDACE,uCACF,CAEA,4BACE,UACF","sources":["../node_modules/antd/dist/reset.css","styles/LayoutStyles.css","index.css"],"sourcesContent":["/* stylelint-disable */\nhtml,\nbody {\n width: 100%;\n height: 100%;\n}\ninput::-ms-clear,\ninput::-ms-reveal {\n display: none;\n}\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n@-ms-viewport {\n width: device-width;\n}\nbody {\n margin: 0;\n}\n[tabindex='-1']:focus {\n outline: none;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n margin-top: 0;\n margin-bottom: 0.5em;\n font-weight: 500;\n}\np {\n margin-top: 0;\n margin-bottom: 1em;\n}\nabbr[title],\nabbr[data-original-title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline;\n text-decoration: underline dotted;\n border-bottom: 0;\n cursor: help;\n}\naddress {\n margin-bottom: 1em;\n font-style: normal;\n line-height: inherit;\n}\ninput[type='text'],\ninput[type='password'],\ninput[type='number'],\ntextarea {\n -webkit-appearance: none;\n}\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1em;\n}\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\ndt {\n font-weight: 500;\n}\ndd {\n margin-bottom: 0.5em;\n margin-left: 0;\n}\nblockquote {\n margin: 0 0 1em;\n}\ndfn {\n font-style: italic;\n}\nb,\nstrong {\n font-weight: bolder;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\npre,\ncode,\nkbd,\nsamp {\n font-size: 1em;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n}\npre {\n margin-top: 0;\n margin-bottom: 1em;\n overflow: auto;\n}\nfigure {\n margin: 0 0 1em;\n}\nimg {\n vertical-align: middle;\n border-style: none;\n}\na,\narea,\nbutton,\n[role='button'],\ninput:not([type='range']),\nlabel,\nselect,\nsummary,\ntextarea {\n touch-action: manipulation;\n}\ntable {\n border-collapse: collapse;\n}\ncaption {\n padding-top: 0.75em;\n padding-bottom: 0.3em;\n text-align: left;\n caption-side: bottom;\n}\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n color: inherit;\n font-size: inherit;\n font-family: inherit;\n line-height: inherit;\n}\nbutton,\ninput {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml [type='button'],\n[type='reset'],\n[type='submit'] {\n -webkit-appearance: button;\n}\nbutton::-moz-focus-inner,\n[type='button']::-moz-focus-inner,\n[type='reset']::-moz-focus-inner,\n[type='submit']::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\ninput[type='radio'],\ninput[type='checkbox'] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type='date'],\ninput[type='time'],\ninput[type='datetime-local'],\ninput[type='month'] {\n -webkit-appearance: listbox;\n}\ntextarea {\n overflow: auto;\n resize: vertical;\n}\nfieldset {\n min-width: 0;\n margin: 0;\n padding: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n margin-bottom: 0.5em;\n padding: 0;\n color: inherit;\n font-size: 1.5em;\n line-height: inherit;\n white-space: normal;\n}\nprogress {\n vertical-align: baseline;\n}\n[type='number']::-webkit-inner-spin-button,\n[type='number']::-webkit-outer-spin-button {\n height: auto;\n}\n[type='search'] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n[type='search']::-webkit-search-cancel-button,\n[type='search']::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\noutput {\n display: inline-block;\n}\nsummary {\n display: list-item;\n}\ntemplate {\n display: none;\n}\n[hidden] {\n display: none !important;\n}\nmark {\n padding: 0.2em;\n background-color: #feffe6;\n}\n","/* Light mode styles */\r\n.main {\r\n padding: 20px;\r\n height: 100vh;\r\n background-color: #fff;\r\n color: #3d3d3d;\r\n}\r\n\r\n.layout {\r\n display: flex;\r\n}\r\n\r\n.sidebar {\r\n min-height: 760px;\r\n height: auto;\r\n width: 300px;\r\n border-radius: 8px;\r\n background-color: #f7f7f7;\r\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\r\n margin-right: 20px;\r\n color: #3d3d3d;\r\n font-size: 1.1rem;\r\n font-weight: 600;\r\n}\r\n\r\n.content {\r\n width: 100%;\r\n height: 100%;\r\n}\r\n\r\n.header {\r\n height: 60px;\r\n margin-bottom: 20px;\r\n border-radius: 8px;\r\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\r\n background-color: #fff;\r\n display: flex;\r\n align-items: center;\r\n padding: 0 20px;\r\n}\r\n\r\n.body {\r\n height: calc(100% - 80px);\r\n border-radius: 8px;\r\n margin-bottom: 20px;\r\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\r\n background-color: #fff;\r\n overflow-y: auto;\r\n padding: 20px;\r\n}\r\n\r\n.logo h6 {\r\n font-size: 1.5rem;\r\n text-align: center;\r\n margin: 20px 0px;\r\n color: #3d3d3d;\r\n font-weight: 700;\r\n letter-spacing: 2px;\r\n}\r\n\r\n.menu {\r\n margin-top: 50px;\r\n}\r\n\r\n.menu-item {\r\n margin-top: 25px;\r\n display: flex;\r\n align-items: center;\r\n padding: 10px 20px;\r\n border-radius: 8px;\r\n transition: all 0.3s ease;\r\n cursor: pointer;\r\n}\r\n\r\n.menu-item:hover {\r\n background-color: grey;\r\n}\r\n\r\n.menu-item a {\r\n color: #3d3d3d;\r\n text-decoration: none;\r\n margin-left: 20px;\r\n}\r\n\r\n.menu-item i {\r\n font-size: 1.2rem;\r\n margin-right: 20px;\r\n color: #3d3d3d;\r\n}\r\n\r\n.active {\r\n background-color: #252525;\r\n color: #fff;\r\n}\r\n\r\n.active a,\r\n.active i {\r\n color: #fff;\r\n}\r\n\r\n.header-content {\r\n display: flex;\r\n align-items: center;\r\n height: 100%;\r\n justify-content: flex-end;\r\n margin-right: 20px;\r\n}\r\n\r\n.header-content i {\r\n margin-right: 20px;\r\n font-size: 1.2rem;\r\n color: #3d3d3d;\r\n}\r\n\r\n.header-content a {\r\n text-decoration: none;\r\n font-size: 1.1rem;\r\n color: #3d3d3d;\r\n margin: 0 10px;\r\n text-transform: uppercase;\r\n font-weight: 600;\r\n letter-spacing: 1px;\r\n}\r\n\r\n/*****************/\r\n.date-picker {\r\n width: 90%;\r\n margin: 0 auto;\r\n}\r\n\r\n.time-picker {\r\n width: 90%;\r\n margin: 0 auto;\r\n}\r\n\r\n.d-flex-center {\r\n display: flex;\r\n justify-content: center;\r\n}\r\n.appoint-card-body {\r\n display: flex;\r\n background-color: #f7f7f7;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 8px;\r\n}\r\n.card-body {\r\n background-color: #f7f7f7;\r\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);\r\n width: auto;\r\n}\r\n.user-doctor-admin-name {\r\n margin-top: 8px;\r\n}\r\n",".spinner {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n z-index: 100;\r\n}\r\n\r\n.spinner-border {\r\n height: 50px;\r\n width: 50px;\r\n}\r\n\r\n/*register page*/\r\n\r\n.register-page {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n height: 90vh;\r\n}\r\n\r\n.register-page form {\r\n width: 400px;\r\n background-color: #fff;\r\n padding: 40px;\r\n border-radius: 10px;\r\n box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\r\n}\r\n.register-page form .d-flex {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-top: 20px;\r\n}\r\n\r\n.register-page form .d-flex a {\r\n color: #007bff;\r\n text-decoration: none;\r\n}\r\n\r\n.register-page h1 {\r\n text-align: center;\r\n margin-bottom: 20px;\r\n}\r\n\r\n.register-page .ant-form-item {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.register-page .ant-form-item-label {\r\n font-weight: bold;\r\n}\r\n\r\n.register-page .ant-form-item-explain {\r\n color: #ff4d4f;\r\n}\r\n\r\n.register-page .ant-form-item-has-error .ant-input {\r\n border-color: #ff4d4f;\r\n}\r\n\r\n.register-page .ant-form-item-has-error .ant-input:focus {\r\n box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);\r\n}\r\n\r\n.register-page .btn-primary {\r\n width: 100%;\r\n}\r\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | const userModel = require('../models/userModel'); 3 | const doctorModel = require("../models/doctorModel"); 4 | const appointmentModel = require("../models/appointmentModel"); 5 | const jwt = require("jsonwebtoken"); 6 | const moment = require("moment"); 7 | 8 | // login callback 9 | const loginController = async (req, res) => { 10 | try { 11 | const { email, password } = req.body; 12 | const user = await userModel.findOne({ email }); 13 | if (!user) { 14 | return res.status(404).send('User Not Found'); 15 | } 16 | // Compare hashed password 17 | const match = await bcrypt.compare(password, user.password); 18 | if (!match) { 19 | return res.status(401).send('Invalid Password'); 20 | } 21 | const token = jwt.sign({id: user._id}, process.env.JWT_SECRET,{expiresIn:"1d"},); 22 | res.status(200).send({ message: "Login Success", success: true, token }); 23 | } catch (error) { 24 | console.log(error); 25 | console.log(process.env.JWT_SECRET); 26 | res.status(500).send({ message: `Error in Login CTRL ${error.message}` }); 27 | } 28 | }; 29 | 30 | //Register Callback 31 | const registerController = async (req, res) => { 32 | try { 33 | const { name, email, password } = req.body; 34 | 35 | // Hash and salt password 36 | const saltRounds = 10; 37 | const hashedPassword = await bcrypt.hash(password, saltRounds); 38 | 39 | const newUser = new userModel({ 40 | name, 41 | email, 42 | password: hashedPassword, 43 | }); 44 | await newUser.save(); 45 | 46 | res.status(201).json({ 47 | success: true, 48 | newUser, 49 | }); 50 | } catch (error) { 51 | res.status(400).json({ 52 | success: false, 53 | error, 54 | }); 55 | } 56 | }; 57 | 58 | const authController = async (req, res) => { 59 | try { 60 | const user = await userModel.findById(req.body.userId); 61 | if (!user) { 62 | return res.status(200).send({ 63 | message: "user not found", 64 | success: false, 65 | }); 66 | } else { 67 | user.password = undefined; // move this line here 68 | res.status(200).send({ 69 | success: true, 70 | data: user, 71 | }); 72 | } 73 | } catch (error) { 74 | console.log(error); 75 | res.status(500).send({ 76 | message: "auth error", 77 | success: false, 78 | error, 79 | }); 80 | } 81 | }; 82 | 83 | // Appply Doctor Controller 84 | const applyDoctorController = async (req, res) => { 85 | try { 86 | const newDoctor = await doctorModel({ ...req.body, status: "pending" }); 87 | await newDoctor.save(); 88 | const adminUser = await userModel.findOne({ isAdmin: true }); 89 | const notification = adminUser.notification; 90 | notification.push({ 91 | type: "apply-doctor-request", 92 | message: `${newDoctor.firstName} ${newDoctor.lastName} Has Applied For A Doctor Account`, 93 | data: { 94 | doctorId: newDoctor._id, 95 | name: newDoctor.firstName + " " + newDoctor.lastName, 96 | onClickPath: "/admin/doctors", 97 | }, 98 | }); 99 | await userModel.findByIdAndUpdate(adminUser._id, { notification }); 100 | res.status(201).send({ 101 | success: true, 102 | message: "Doctor Account Applied Successfully", 103 | }); 104 | } catch (error) { 105 | console.log(error); 106 | res.status(500).send({ 107 | success: false, 108 | error, 109 | message: "Error While Applying For Doctor", 110 | }); 111 | } 112 | }; 113 | 114 | // Notification controller 115 | const getAllNotificationController = async (req, res) => { 116 | try { 117 | const user = await userModel.findOne({ _id: req.body.userId }); 118 | const seennotification = user.seennotification; 119 | const notification = user.notification; 120 | seennotification.push(...notification); 121 | user.notification = []; 122 | user.seennotification = notification; 123 | const updatedUser = await user.save(); 124 | res.status(200).send({ 125 | success: true, 126 | message: "All Notifications Marked As Read", 127 | data: updatedUser, 128 | }); 129 | } catch (error) { 130 | console.log(error); 131 | res.status(500).send({ 132 | message: "Error In Notification", 133 | success: false, 134 | error, 135 | }); 136 | } 137 | }; 138 | 139 | // delete notifications 140 | const deleteAllNotificationController = async (req, res) => { 141 | try { 142 | const user = await userModel.findOne({ _id: req.body.userId }); 143 | user.notification = []; 144 | user.seennotification = []; 145 | const updatedUser = await user.save(); 146 | updatedUser.password = undefined; 147 | res.status(200).send({ 148 | success: true, 149 | message: "Notifications Deleted Successfully", 150 | data: updatedUser, 151 | }); 152 | } catch (error) { 153 | console.log(error); 154 | res.status(500).send({ 155 | success: false, 156 | message: "Unable To Delete All Notifications", 157 | error, 158 | }); 159 | } 160 | }; 161 | 162 | //GET ALL DOC 163 | const getAllDocotrsController = async (req, res) => { 164 | try { 165 | const doctors = await doctorModel.find({ status: "approved" }); 166 | res.status(200).send({ 167 | success: true, 168 | message: "Doctors Lists Fetched Successfully", 169 | data: doctors, 170 | }); 171 | } catch (error) { 172 | console.log(error); 173 | res.status(500).send({ 174 | success: false, 175 | error, 176 | message: "Error While Fetching Doctor", 177 | }); 178 | } 179 | }; 180 | 181 | // Checking Availability 182 | const bookingAvailabilityController = async (req, res) => { 183 | try { 184 | const date = moment(req.body.date, "DD-MM-YYYY").toISOString(); 185 | const startTime = moment(req.body.time, "HH:mm").toISOString(); 186 | const doctorId = req.body.doctorId; 187 | const doctor = await doctorModel.findById(doctorId); 188 | if (!doctor) { 189 | return res.status(404).send({ 190 | message: "Doctor not found", 191 | success: false, 192 | }); 193 | } 194 | const start = moment(doctor.starttime, "HH:mm").toISOString(); 195 | const end = moment(doctor.endtime, "HH:mm").toISOString(); 196 | if (!moment(startTime).isBetween(start, end, undefined, "[]")) { 197 | return res.status(200).send({ 198 | message: "Appointment Not Available", 199 | success: false, 200 | }); 201 | } 202 | const appointments = await appointmentModel.find({ 203 | doctorId, 204 | date, 205 | time: startTime, 206 | }); 207 | if (appointments.length > 0) { 208 | return res.status(200).send({ 209 | message: "Appointment Not Available", 210 | success: false, 211 | }); 212 | } 213 | return res.status(200).send({ 214 | success: true, 215 | message: "Appointment Available", 216 | }); 217 | } catch (error) { 218 | console.log(error); 219 | res.status(500).send({ 220 | success: false, 221 | error, 222 | message: "Error Checking Appointment Availability", 223 | }); 224 | } 225 | }; 226 | 227 | 228 | //BOOK APPOINTMENT 229 | /*const bookAppointmentController = async (req, res) => { 230 | try { 231 | const date = moment(req.body.date, "DD-MM-YYYY").toISOString(); 232 | const startTime = moment(req.body.time, "HH:mm").toISOString(); 233 | const doctorId = req.body.doctorId; 234 | const doctor = await doctorModel.findById(doctorId); 235 | if (!doctor) { 236 | return res.status(404).send({ 237 | message: "Doctor not found", 238 | success: false, 239 | }); 240 | } 241 | const start = moment(doctor.starttime, "HH:mm").toISOString(); 242 | const end = moment(doctor.endtime, "HH:mm").toISOString(); 243 | if (!moment(startTime).isBetween(start, end, undefined, "[]")) { 244 | return res.status(400).send({ 245 | message: "Selected time is not within doctor's available range", 246 | success: false, 247 | }); 248 | } 249 | const appointments = await appointmentModel.find({ 250 | doctorId, 251 | date, 252 | }); 253 | if (appointments.length >= doctor.maxPatientsPerDay) { 254 | return res.status(400).send({ 255 | message: "Maximum number of appointments reached for this day", 256 | success: false, 257 | }); 258 | } 259 | const newAppointment = new appointmentModel({ 260 | doctorId, 261 | userId: req.body.userId, 262 | date, 263 | time: startTime, 264 | doctorInfo: req.body.doctorInfo, 265 | userInfo: req.body.userInfo, 266 | }); 267 | await newAppointment.save(); 268 | return res.status(200).send({ 269 | success: true, 270 | message: "Appointment Booked Successfully", 271 | }); 272 | } catch (error) { 273 | console.log(error); 274 | res.status(500).send({ 275 | success: false, 276 | error, 277 | message: "Error In Booking Appointment", 278 | }); 279 | } 280 | };*/ 281 | 282 | const bookAppointmentController = async (req, res) => { 283 | try { 284 | const date = moment(req.body.date, "DD-MM-YYYY").toISOString(); 285 | const startTime = moment(req.body.time, "HH:mm").toISOString(); 286 | const doctorId = req.body.doctorId; 287 | const doctor = await doctorModel.findById(doctorId); 288 | if (!doctor) { 289 | return res.status(404).send({ 290 | message: "Doctor Not Found", 291 | success: false, 292 | }); 293 | } 294 | const start = moment(doctor.starttime, "HH:mm").toISOString(); 295 | const end = moment(doctor.endtime, "HH:mm").toISOString(); 296 | if (!moment(startTime).isBetween(start, end, undefined, "[]")) { 297 | return res.status(400).send({ 298 | message: "Selected Time Is Not Within Doctor's Available Range", 299 | success: false, 300 | }); 301 | } 302 | const appointments = await appointmentModel.find({ 303 | doctorId, 304 | date, 305 | status: "approved" 306 | }); 307 | if (appointments.length >= doctor.maxPatientsPerDay) { 308 | return res.status(400).send({ 309 | message: "Maximum Number Of Appointments Reached For This Day", 310 | success: false, 311 | }); 312 | } 313 | const existingAppointment = await appointmentModel.findOne({ 314 | doctorId, 315 | date, 316 | time: startTime, 317 | status: "approved" 318 | }); 319 | if (existingAppointment) { 320 | return res.status(400).send({ 321 | message: "Appointment Already Booked For This Time Slot", 322 | success: false, 323 | }); 324 | } 325 | const newAppointment = new appointmentModel({ 326 | doctorId, 327 | userId: req.body.userId, 328 | date, 329 | time: startTime, 330 | doctorInfo: req.body.doctorInfo, 331 | userInfo: req.body.userInfo, 332 | }); 333 | await newAppointment.save(); 334 | return res.status(200).send({ 335 | success: true, 336 | message: "Appointment Booked Successfully", 337 | }); 338 | } catch (error) { 339 | console.log(error); 340 | res.status(500).send({ 341 | success: false, 342 | error, 343 | message: "Error In Booking Appointment", 344 | }); 345 | } 346 | }; 347 | 348 | 349 | 350 | 351 | 352 | const userAppointmentsController = async (req, res) => { 353 | try { 354 | const appointments = await appointmentModel.find({ 355 | userId: req.body.userId, 356 | }); 357 | res.status(200).send({ 358 | success: true, 359 | message: "Users Appointments Fetch Successfully", 360 | data: appointments, 361 | }); 362 | } catch (error) { 363 | console.log(error); 364 | res.status(500).send({ 365 | success: false, 366 | error, 367 | message: "Error In User Appointments", 368 | }); 369 | } 370 | }; 371 | 372 | module.exports = { loginController, registerController, authController , applyDoctorController, getAllNotificationController, deleteAllNotificationController, getAllDocotrsController, bookAppointmentController, bookingAvailabilityController, userAppointmentsController}; --------------------------------------------------------------------------------