diff --git a/client/src/layouts/dashboard/config-navigation.jsx b/client/src/layouts/dashboard/config-navigation.jsx index cf86a11..1c990b4 100644 --- a/client/src/layouts/dashboard/config-navigation.jsx +++ b/client/src/layouts/dashboard/config-navigation.jsx @@ -2,49 +2,56 @@ import SvgColor from 'src/components/svg-color'; // ---------------------------------------------------------------------- -const icon = (name) => ; +const icon = (name) => ( + +); const navConfig = [ { title: 'dashboard', path: '/', - icon: icon('ic_analytics') + icon: icon('ic_analytics'), }, { title: 'user', path: '/user', - icon: icon('ic_user') + icon: icon('ic_user'), }, { title: 'product', path: '/products', - icon: icon('ic_cart') + icon: icon('ic_cart'), }, { title: 'blog', path: '/blog', - icon: icon('ic_blog') + icon: icon('ic_blog'), }, { title: 'login', path: '/login', - icon: icon('ic_lock') + icon: icon('ic_lock'), }, { title: 'Register', path: '/register', - icon: icon('ic_lock') + icon: icon('ic_lock'), }, { title: 'Doctors', path: '/doctors', - icon: icon('ic_user') + icon: icon('ic_user'), + }, + { + title: 'Patients', + path: '/patients', + icon: icon('ic_user'), }, { title: 'Not found', path: '/404', - icon: icon('ic_disabled') - } + icon: icon('ic_disabled'), + }, ]; export default navConfig; diff --git a/client/src/pages/health-record.jsx b/client/src/pages/health-record.jsx index 6631844..63cb859 100644 --- a/client/src/pages/health-record.jsx +++ b/client/src/pages/health-record.jsx @@ -1,17 +1,19 @@ import { Helmet } from 'react-helmet-async'; +import { useParams } from 'react-router-dom'; import { HealthRecordView } from 'src/sections/healthRecords'; // ---------------------------------------------------------------------- export default function HealthRecordPage() { + let { patientID } = useParams(); return ( <> Health Records - + ); } diff --git a/client/src/pages/patients.jsx b/client/src/pages/patients.jsx new file mode 100644 index 0000000..5ec6912 --- /dev/null +++ b/client/src/pages/patients.jsx @@ -0,0 +1,17 @@ +import { Helmet } from 'react-helmet-async'; + +import { PatientView } from 'src/sections/patients/view'; + +// ---------------------------------------------------------------------- + +export default function PatientPage() { + return ( + <> + + Patients + + + + + ); +} diff --git a/client/src/routes/sections.jsx b/client/src/routes/sections.jsx index dbb8eb6..988dfb5 100644 --- a/client/src/routes/sections.jsx +++ b/client/src/routes/sections.jsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'react'; -import { Outlet, Navigate, useRoutes } from 'react-router-dom'; +import { Outlet, Navigate, useRoutes, useParams } from 'react-router-dom'; import DashboardLayout from 'src/layouts/dashboard'; @@ -9,6 +9,7 @@ export const IndexPage = lazy(() => import('src/pages/app')); export const BlogPage = lazy(() => import('src/pages/blog')); export const UserPage = lazy(() => import('src/pages/user')); export const DoctorsPage = lazy(() => import('src/pages/doctors')); +export const PatientsPage = lazy(() => import('src/pages/patients')); export const LoginPage = lazy(() => import('src/pages/login')); export const RegisterPage = lazy(() => import('src/pages/register')); export const ProductsPage = lazy(() => import('src/pages/products')); @@ -34,6 +35,8 @@ export default function Router() { { path: 'products', element: }, { path: 'blog', element: }, { path: 'doctors', element: }, + { path: 'patients', element: }, + { path: 'health-record/:patientID', element: }, { path: 'health-record', element: }, ], }, diff --git a/client/src/sections/healthRecords/health-record-view.jsx b/client/src/sections/healthRecords/health-record-view.jsx index 1703e80..7bbb79f 100644 --- a/client/src/sections/healthRecords/health-record-view.jsx +++ b/client/src/sections/healthRecords/health-record-view.jsx @@ -5,18 +5,32 @@ import Typography from '@mui/material/Typography'; import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; -import HealthRecordSummary from './health-record-summary'; +import { useUserContext } from 'src/contexts/userContext'; import { axiosInstance } from '../../utils/axiosInstance'; -export default function HealthRecordView() { +import HealthRecordSummary from './health-record-summary'; + +export default function HealthRecordView({ patientID }) { + console.log(patientID); const [healthRecords, setHealthRecords] = useState([]); const [newName, setNewName] = useState(''); const [newRecord, setNewRecord] = useState(''); + // const { + // user: { role }, + // } = useUserContext(); + const user = localStorage.getItem('userRole'); + console.log(user); + useEffect(() => { const fetchHealthRecords = async () => { try { - const res = await axiosInstance.get(`/patients/6548e928d2f717cbd465119a/medicalhistory`); + let res; + if (user === 'Doctor') { + res = await axiosInstance.get(`/patients/${patientID}/medicalhistory`); + } else { + res = await axiosInstance.get(`/me/medicalhistory`); + } setHealthRecords(res.data.result); } catch (err) { console.log(err); @@ -27,7 +41,7 @@ export default function HealthRecordView() { const handleSubmit = async () => { try { - await axiosInstance.post('/patients/6548e928d2f717cbd465119a/medicalhistory', { + await axiosInstance.post(`/patients/${patientID}/medicalhistory`, { name: newName, medicalRecord: newRecord, }); @@ -55,29 +69,33 @@ export default function HealthRecordView() { ))} - - Add New Health Record - + {user === 'Doctor' && ( + + Add New Health Record + + )} -
- setNewName(event.target.value)} - sx={{ mb: 2 }} - /> -
- setNewRecord(event.target.value)} - sx={{ mb: 2 }} - /> -
- -
+ {user === 'Doctor' && ( +
+ setNewName(event.target.value)} + sx={{ mb: 2 }} + /> +
+ setNewRecord(event.target.value)} + sx={{ mb: 2 }} + /> +
+ +
+ )} ); } diff --git a/client/src/sections/login/login-view.jsx b/client/src/sections/login/login-view.jsx index 0a7aa8c..9b7a420 100644 --- a/client/src/sections/login/login-view.jsx +++ b/client/src/sections/login/login-view.jsx @@ -36,7 +36,7 @@ export default function LoginView() { const { register, handleSubmit, - formState: { errors } + formState: { errors }, } = useForm(); const [loading, setLoading] = useState(false); @@ -51,7 +51,9 @@ export default function LoginView() { if (res.status == 200) { const user = res.data.user; - setUser({ name: user.name, role: user.role }); + // setUser({ name: user.name, role: user.role }); + localStorage.setItem('userRole', user.role); + localStorage.setItem('userName', user.name); router.push(destination); } else { @@ -69,16 +71,16 @@ export default function LoginView() { sx={{ ...bgGradient({ color: alpha(theme.palette.background.default, 0.9), - imgUrl: '/assets/background/overlay_4.jpg' + imgUrl: '/assets/background/overlay_4.jpg', }), - height: 1 + height: 1, }} > @@ -87,7 +89,7 @@ export default function LoginView() { sx={{ p: 5, width: 1, - maxWidth: 420 + maxWidth: 420, }} > Sign in to Minimal @@ -110,7 +112,7 @@ export default function LoginView() { - ) + ), }} /> diff --git a/client/src/sections/patients/table-empty-rows.jsx b/client/src/sections/patients/table-empty-rows.jsx new file mode 100644 index 0000000..ed44fe1 --- /dev/null +++ b/client/src/sections/patients/table-empty-rows.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; + +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; + +// ---------------------------------------------------------------------- + +export default function TableEmptyRows({ emptyRows, height }) { + if (!emptyRows) { + return null; + } + + return ( + + + + ); +} + +TableEmptyRows.propTypes = { + emptyRows: PropTypes.number, + height: PropTypes.number, +}; diff --git a/client/src/sections/patients/table-no-data.jsx b/client/src/sections/patients/table-no-data.jsx new file mode 100644 index 0000000..aedae4b --- /dev/null +++ b/client/src/sections/patients/table-no-data.jsx @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; + +import Paper from '@mui/material/Paper'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; +import Typography from '@mui/material/Typography'; + +// ---------------------------------------------------------------------- + +export default function TableNoData({ query }) { + return ( + + + + + Not found + + + + No results found for   + "{query}". +
Try checking for typos or using complete words. +
+
+
+
+ ); +} + +TableNoData.propTypes = { + query: PropTypes.string, +}; diff --git a/client/src/sections/patients/user-table-head.jsx b/client/src/sections/patients/user-table-head.jsx new file mode 100644 index 0000000..cdb9a47 --- /dev/null +++ b/client/src/sections/patients/user-table-head.jsx @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; + +import Box from '@mui/material/Box'; +import TableRow from '@mui/material/TableRow'; +import Checkbox from '@mui/material/Checkbox'; +import TableHead from '@mui/material/TableHead'; +import TableCell from '@mui/material/TableCell'; +import TableSortLabel from '@mui/material/TableSortLabel'; + +import { visuallyHidden } from './utils'; + +// ---------------------------------------------------------------------- + +export default function UserTableHead({ + order, + orderBy, + rowCount, + headLabel, + numSelected, + onRequestSort, + onSelectAllClick, +}) { + const onSort = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + /> + + + {headLabel.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ))} + + + ); +} + +UserTableHead.propTypes = { + order: PropTypes.oneOf(['asc', 'desc']), + orderBy: PropTypes.string, + rowCount: PropTypes.number, + headLabel: PropTypes.array, + numSelected: PropTypes.number, + onRequestSort: PropTypes.func, + onSelectAllClick: PropTypes.func, +}; diff --git a/client/src/sections/patients/user-table-row.jsx b/client/src/sections/patients/user-table-row.jsx new file mode 100644 index 0000000..5ed0260 --- /dev/null +++ b/client/src/sections/patients/user-table-row.jsx @@ -0,0 +1,115 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; + +import Stack from '@mui/material/Stack'; +import Avatar from '@mui/material/Avatar'; +import Popover from '@mui/material/Popover'; +import TableRow from '@mui/material/TableRow'; +import Checkbox from '@mui/material/Checkbox'; +import MenuItem from '@mui/material/MenuItem'; +import TableCell from '@mui/material/TableCell'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; + +import Label from 'src/components/label'; +import Iconify from 'src/components/iconify'; + +// ---------------------------------------------------------------------- + +export default function UserTableRow({ + id, + selected, + name, + avatarUrl, + phone, + email, + gender, + isVerified, + status, + handleClick, + viewProfile, + viewHealthRecords, +}) { + const [open, setOpen] = useState(null); + + const handleOpenMenu = (event) => { + setOpen(event.currentTarget); + }; + + const handleCloseMenu = () => { + setOpen(null); + }; + + return ( + <> + + + + + + + + + + {name} + + + + + {phone} + + {email} + + {gender} + + {/* {isVerified ? 'Yes' : 'No'} */} + + {/* + + */} + + + + + + + + + + + + Profile + + + + + Health Records + + + + ); +} + +UserTableRow.propTypes = { + id: PropTypes.any, + avatarUrl: PropTypes.any, + phone: PropTypes.any, + handleClick: PropTypes.func, + viewProfile: PropTypes.func, + viewHealthRecords: PropTypes.func, + isVerified: PropTypes.any, + name: PropTypes.any, + email: PropTypes.any, + gender: PropTypes.any, + selected: PropTypes.any, + status: PropTypes.string, +}; diff --git a/client/src/sections/patients/user-table-toolbar.jsx b/client/src/sections/patients/user-table-toolbar.jsx new file mode 100644 index 0000000..169ce33 --- /dev/null +++ b/client/src/sections/patients/user-table-toolbar.jsx @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; + +import Tooltip from '@mui/material/Tooltip'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import InputAdornment from '@mui/material/InputAdornment'; + +import Iconify from 'src/components/iconify'; + +// ---------------------------------------------------------------------- + +export default function UserTableToolbar({ numSelected, filterName, onFilterName }) { + return ( + theme.spacing(0, 1, 0, 3), + ...(numSelected > 0 && { + color: 'primary.main', + bgcolor: 'primary.lighter', + }), + }} + > + {numSelected > 0 ? ( + + {numSelected} selected + + ) : ( + + + + } + /> + )} + + {numSelected > 0 ? ( + + + + + + ) : ( + + + + + + )} + + ); +} + +UserTableToolbar.propTypes = { + numSelected: PropTypes.number, + filterName: PropTypes.string, + onFilterName: PropTypes.func, +}; diff --git a/client/src/sections/patients/utils.js b/client/src/sections/patients/utils.js new file mode 100644 index 0000000..18fe17c --- /dev/null +++ b/client/src/sections/patients/utils.js @@ -0,0 +1,56 @@ +export const visuallyHidden = { + border: 0, + margin: -1, + padding: 0, + width: '1px', + height: '1px', + overflow: 'hidden', + position: 'absolute', + whiteSpace: 'nowrap', + clip: 'rect(0 0 0 0)', +}; + +export function emptyRows(page, rowsPerPage, arrayLength) { + return page ? Math.max(0, (1 + page) * rowsPerPage - arrayLength) : 0; +} + +function descendingComparator(a, b, orderBy) { + if (a[orderBy] === null) { + return 1; + } + if (b[orderBy] === null) { + return -1; + } + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} +export function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +export function applyFilter({ inputData, comparator, filterName }) { + const stabilizedThis = inputData.map((el, index) => [el, index]); + + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + + inputData = stabilizedThis.map((el) => el[0]); + + if (filterName) { + inputData = inputData.filter( + (user) => user.name.toLowerCase().indexOf(filterName.toLowerCase()) !== -1 + ); + } + + return inputData; +} diff --git a/client/src/sections/patients/view/index.js b/client/src/sections/patients/view/index.js new file mode 100644 index 0000000..df2e262 --- /dev/null +++ b/client/src/sections/patients/view/index.js @@ -0,0 +1 @@ +export { default as PatientView } from './patient-view'; diff --git a/client/src/sections/patients/view/patient-view.jsx b/client/src/sections/patients/view/patient-view.jsx new file mode 100644 index 0000000..9b4b002 --- /dev/null +++ b/client/src/sections/patients/view/patient-view.jsx @@ -0,0 +1,207 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import Card from '@mui/material/Card'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import TableBody from '@mui/material/TableBody'; +import Typography from '@mui/material/Typography'; +import TableContainer from '@mui/material/TableContainer'; +import TablePagination from '@mui/material/TablePagination'; + +import { axiosInstance } from '../../../utils/axiosInstance'; + +import { users } from 'src/_mock/user'; + +import Iconify from 'src/components/iconify'; +import Scrollbar from 'src/components/scrollbar'; + +import TableNoData from '../table-no-data'; +import UserTableRow from '../user-table-row'; +import UserTableHead from '../user-table-head'; +import TableEmptyRows from '../table-empty-rows'; +import UserTableToolbar from '../user-table-toolbar'; +import { emptyRows, applyFilter, getComparator } from '../utils'; + +// ---------------------------------------------------------------------- + +export default function UserPage() { + const [patients, setPatients] = useState([]); + + useEffect(() => { + const fetchMyPatients = async () => { + try { + const res = await axiosInstance.get(`/patients`); + setPatients(res.data.result); + } catch (err) { + console.log(err); + } + }; + fetchMyPatients(); + }, []); + + //'/assets/images/avatars/avatar_1.jpg' + //console.log(users); + + const [page, setPage] = useState(0); + + const [order, setOrder] = useState('asc'); + + const [selected, setSelected] = useState([]); + + const [orderBy, setOrderBy] = useState('name'); + + const [filterName, setFilterName] = useState(''); + + const [rowsPerPage, setRowsPerPage] = useState(5); + + const handleSort = (event, id) => { + const isAsc = orderBy === id && order === 'asc'; + if (id !== '') { + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(id); + } + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = patients.map((n) => n.name); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + setSelected(newSelected); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setPage(0); + setRowsPerPage(parseInt(event.target.value, 10)); + }; + + const handleFilterByName = (event) => { + setPage(0); + setFilterName(event.target.value); + }; + + const dataFiltered = applyFilter({ + inputData: patients, + comparator: getComparator(order, orderBy), + filterName, + }); + + const navigate = useNavigate(); + + const handleViewHealthRecords = (patientID) => { + navigate(`/health-record/${patientID}`); + }; + + const notFound = !dataFiltered.length && !!filterName; + + return ( + + + Patients + + + + + + + + + + + + + {dataFiltered + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row) => ( + handleClick(event, row.name)} + viewProfile={() => console.log('view profile')} + viewHealthRecords={() => { + console.log('view health records' + row._id); + handleViewHealthRecords(row._id); + }} + /> + ))} + + + + {notFound && } + +
+
+
+ + +
+
+ ); +} diff --git a/server/src/routes/user.route.ts b/server/src/routes/user.route.ts index bb9f8d6..35a232e 100644 --- a/server/src/routes/user.route.ts +++ b/server/src/routes/user.route.ts @@ -9,7 +9,7 @@ const router = express.Router(); // Just for testing router.use(isAuthenticated); -router.use(isAuthorized('Admin')); +// router.use(isAuthorized('Admin')); router.delete('/:id', (req: Request, res: Response) => { controller(res)(deleteUsers)({ _id: req.params.id }); diff --git a/server/src/services/doctor.service.ts b/server/src/services/doctor.service.ts index 08b7366..25f973f 100644 --- a/server/src/services/doctor.service.ts +++ b/server/src/services/doctor.service.ts @@ -1,13 +1,13 @@ const { v4: uuidv4 } = require('uuid'); import { HttpError } from '../utils'; import StatusCodes from 'http-status-codes'; -import { User, Contract, Appointment, IPatient, IDoctor, Doctor, Request } from '../models'; +import { User, Contract, Appointment, IPatient, IDoctor, Doctor, Request, Patient } from '../models'; const getMyPatients = async (query: any) => { - const appointments = await Appointment.find(query).distinct('patientID').select('patientID').populate('patientID'); - if (!appointments) return new HttpError(StatusCodes.NOT_FOUND, 'No patients with this doctor'); + const patientIDs = await Appointment.find(query).distinct('patientID'); + if (!patientIDs) return new HttpError(StatusCodes.NOT_FOUND, 'No patients with this doctor'); - const patients = appointments.map((appointment: any) => appointment.patientID); + const patients = await Patient.find({ _id: { $in: patientIDs } }); return { status: StatusCodes.OK,