Skip to content

Online Clinic Platform, part of a complete healthcare system also including a Pharmacy Platform.

License

Notifications You must be signed in to change notification settings

advanced-computer-lab-2023/Code-of-Duty2-Clinic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

El7a2ni Clinic

A web application which allows Patients to schedule appointments for themselves and their family members and chat or call doctors, Doctors to manage their patient appointments, follow-ups and medical health records, and Admins to manage the users and system as a whole.

It is part of the larger El7a2ni Healthcare Platform, which also includes the El7a2ni Pharmacy web application.

Table of Contents

Badges

MongoDB Express.js ReactJS NodeJS Jira TypeScript MUI Git GitHub JWT Jest

Motivation

Bringing convenience, speed, and ease of use to the forefront of healthcare is our primary objective.

Our application brings a very straight-forward way for patients to interact with doctors and schedule appointments for themselves and their family members and manage their health packages. We also provide the necessary tools for Doctors and Admins to operate the system effectively and efficiently. All of our features are implemented with the user's convenience at mind, and are designed to be intuitive and easy to learn.

We provide all the necessary features that our user types require and eliminate any and all unnecessary clutter, simplifying the processes as much as possible and making it possible to learn and use the system at an extremely rapid rate.

Build Status

  • The project is currently in development.
  • A CI/CD pipeline needs to be implemented.
  • Testing needs to be implemented for the backend using Jest.

Code Style

The code style is enforced using prettier. You can find the code formatting rules in the .prettierrc file.

Project Management

Jira was used as our main issue tracking and project management tool for creating this software.

Jira Jira Jira

Screenshots

Welcome page

Welcome page Welcome page Welcome page

Patient & admin login

Patient & admin login

Patient dashboard

Patient dashboard

Book an appointment

Book an appointment

View the appointment

View the appointment

Request a follow up

Request a follow up

View all doctors

PView all doctors

My doctors

My doctors

Chat with doctor

Chat with doctor

Family members

Family members

Family members details

Family members details

Add register family member

Add register family member

Wallet

Wallet

Doctor login

Doctor login

View doctor's patients

View doctor's patients

Add available time slots

Add available time slots

Admin dashboard

Admin dashboard

View all admins

View all admins

View all patients

View all patients

Accepted doctor registration request

Accepted doctor registration request

Add, edit, or delete health packages

Add, edit, or delete health packages

Update password

Update password

Tech/Framework used

This software uses the MERN technology stack. Here is a more comprehensive list of used technologies in this software:

Features

The system has four user types: Guest, Patient, Doctor and Admin.

Patient features
  • Account Management:

    • Login, logout, change password, and reset forgotten password via email OTP.
  • Family Management:

    • Link, view, add family members with details.
  • Doctor Interaction:

    • View and interact with doctors (list, search, filter, select).
    • View doctor details.
  • Prescription Handling:

    • View, filter, and select prescriptions.
    • Upload/remove medical documents.
    • Pay for appointments and health packages.
  • Appointment Management:

    • View, filter, and select appointments.
    • Pay for appointments and health packages.
  • Health Records:

    • View uploaded health records.
  • Health Package Subscription:

    • Subscribe, view details, view status, and cancel subscriptions.
  • Wallet Management:

    • View wallet balance.
    • Receive refunds.
  • Communication:

    • Initiate/end video calls.
    • Engage in a chat with doctors.
Doctors features
  • Account Management:

    • Login, logout, change password, and reset forgotten password via email OTP.
  • Profile Management:

    • Edit/update profile details.
  • Patient Interaction:

    • View patient information, patient list, and search/filter patients.
    • Select and manage patients.
  • Appointment Handling:

    • View, receive notifications, reschedule, and cancel appointments.
    • Filter and manage appointments.
  • Time Slot Management:

    • Add, view, and delete available time slots.
  • Health Records:

    • Access uploaded health records.
  • Wallet and Prescription Management:

    • View wallet balance, prescriptions, and manage prescriptions.
    • Initiate/end video calls and chat with patients.
Admin features
  • Account Management:

    • Login, logout, change password, and reset forgotten password via email OTP.
  • Admin Management:

    • Add another admin with a set username and password.
    • Remove user accounts (doctor, patient, or admin) from the system.
  • Information Handling:

    • View uploaded information submitted by doctors for platform entry.
  • Health Package Management:

    • Manage health packages: add, update, or delete with different price ranges (silver, gold, platinum).
  • Doctor Join Requests:

    • Accept or reject doctor requests to join the platform.
Guest features
  • Sign up as a patient.
  • Submit a registration request as a doctor and go through a registration process.

Code Examples

Forget Password Controller
export const resetPasswordRequestHandler = async (req: Request, res: Response) => {
  const { email } = req.body;
  if (!email) return res.status(StatusCodes.BAD_REQUEST).json({ error: "Email is required" });
  try {
    const userData = await sendPasswordResetOTPIfUserExists(email);
    console.log(userData);
    res.status(StatusCodes.OK).json(userData);
  } catch (error: any) {
    return res.status(StatusCodes.BAD_REQUEST).json({ message: error.message });
  }
};

export const deletePasswordResetInfoHandler = async (req: Request, res: Response) => {
  const { email } = req.body;
  if (!email) return res.status(StatusCodes.BAD_REQUEST).json({ error: "Email is required" });
  try {
    const user = await findUserByEmail(email);
    await deletePasswordResetInfo(user);
    res.status(StatusCodes.OK).json({ message: "Password reset info deleted successfully" });
  } catch (error: any) {
    return res.status(StatusCodes.BAD_REQUEST).json({ message: error.message });
  }
};

export const validateOTPHandler = async (req: Request, res: Response) => {
  const { userData, otp } = req.body;
  if (!userData || !otp)
    return res.status(StatusCodes.BAD_REQUEST).json({ error: "UserData and OTP are required" });
  try {
    const user = await validateOTP(userData, otp);
    const passwordResetToken = signAndGetPasswordResetToken({
      id: user._id,
      role: userData.role
    } as User);
    res.cookie("passwordResetToken", passwordResetToken, {
      httpOnly: true,
      expires: user.passwordReset?.expiryDate
    });
    res.status(StatusCodes.OK).json({ message: "Password reset OTP is correct" });
  } catch (error: any) {
    return res.status(StatusCodes.BAD_REQUEST).json({ message: error.message });
  }
};

export const resetPasswordHandler = async (req: Request, res: Response) => {
  const { password, confirmPassword } = req.body;
  if (!password || !confirmPassword)
    res
      .status(StatusCodes.BAD_REQUEST)
      .json({ error: "Both Password and Confirm password is required" });
  if (password !== confirmPassword)
    res
      .status(StatusCodes.BAD_REQUEST)
      .json({ error: "Password and Confirm password must be same" });
  try {
    const { passwordResetToken } = req.cookies;
    if (!passwordResetToken) throw new Error("Password reset token not found");
    const userData = verifyAndDecodePasswordResetToken(passwordResetToken);
    await updatePassword(userData, password);
    res.clearCookie("passwordResetToken", { httpOnly: true });
    res.status(StatusCodes.OK).json({ message: "Password reset successfully" });
  } catch (error: any) {
    return res.status(StatusCodes.BAD_REQUEST).json({ message: error.message });
  }
};
Doctor Add Patient Health Record
export const doctorAddPatientHealthRecord = async (req: AuthorizedRequest, res: Response) => {
  if (!req.user?.id) return res.status(StatusCodes.BAD_REQUEST).send("User id is null");
  const healthRecordAttributes = ["url", "fileType", "recordType", "createdAt"];
  if (!req.params.patientId || !allAttributesExist(healthRecordAttributes, Object.keys(req.body)))
    return res.status(StatusCodes.BAD_REQUEST).send("Attributes missing");

  try {
    res.status(StatusCodes.OK).json(await addHealthRecord(req.params.patientId, req.body));
  } catch (err) {
    res.status(StatusCodes.BAD_REQUEST).send(err);
  }
};
Get Doctor Application Data
const getDoctorRegistrationRequest = async (req: Request, res: Response) => {
  const email = req.params.email;
  try {
    const request = await findDoctorRegistrationRequestByEmail(email);

    if (!request) {
      return res
        .status(StatusCodes.NOT_FOUND)
        .json({ message: "Doctor registration request not found" });
    }

    res.json(request);
  } catch (error) {
    console.error("Error fetching doctor registration request:", error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: "Internal Server Error" });
  }
};

export const getDoctorRegistrationRequestById = async (req: Request, res: Response) => {
  const doctorId = req.params.doctorId;
  try {
    const request = await findDoctorRegistrationRequestById(doctorId);
    if (!request) {
      return res
        .status(StatusCodes.NOT_FOUND)
        .json({ message: "Doctor registration request not found" });
    }
    res.json(request);
  } catch (error) {
    console.error("Error fetching doctor registration request:", error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: "Internal Server Error" });
  }
};

export default getDoctorRegistrationRequest;
Subscribe to Health Package
export const subscribeToHealthPackage = async (req: AuthorizedRequest, res: Response) => {
  const packageId = req.params.packageId;
  const patientId = req.user?.id!;
  const paymentMethod =
    req.query?.paymentMethod === "wallet" ? PaymentMethod.WALLET : PaymentMethod.CREDIT_CARD;
  try {
    await subscribeToHealthPackageService(patientId, patientId, packageId, paymentMethod);
    res.status(200).json({ message: "Subscription added successfully" });
  } catch (error: any) {
    console.error("Error subscribing to health package:", error);
    res.status(400).json({ message: error.message });
  }
};
Medical History Page
export interface IHealthRecord {
  name: string;
  url: string;
  createdAt: string | Date;
  recordType: string;
  fileType?: string;
}
const MedicalHistory: React.FC = () => {
  const [files, setFiles] = useState<Array<IHealthRecord>>([]);
  const [tableLoading, setTableLoading] = useState(true);
  const [deleteLoading, setDeleteLoading] = useState(false);
  const [upload, setUpload] = useState(false);
  const [selected, setSelected] = useState<readonly number[]>([]);
  const [viewFileModal, setViewFileModal] = useState(false);
  const [fileType, setFileType] = useState<string>();
  const [viewFileUrl, setViewFileUrl] = useState<string>();
  const [viewFileName, setViewFileName] = useState<string>();

  useEffect(() => {
    getAllFiles();
  }, []);

  const getAllFiles = () => {
    try {
      axios.get(`${config.serverUri}/patients/health-records`).then((res) => {
        setFiles(res.data);
        setTableLoading(false);
      });
    } catch (error) {}
  };

  const addFileToTable = async (healthrecord?: IHealthRecord) => {
    if (healthrecord)
      setFiles((old) => {
        return [...old, healthrecord];
      });
    setUpload(false);
  };

  const downloadFile = async (url: string, fileName: string) => {
    const response = await fetch(url, { mode: "cors" });
    const blob = await response.blob();
    saveAs(blob, fileName);
  };

  const deleteFile = async (fileUrl: string, index: number) => {
    //setDeleteLoading(true)
    const refd = ref(storage, fileUrl);
    try {
      await deleteObject(refd);
      await axios.delete(`${config.serverUri}/patients/health-records`, {
        params: { fileUrl: fileUrl }
      });
      setFiles((oldFiles) => {
        return [...oldFiles.slice(0, index), ...oldFiles.slice(index + 1)];
      });
    } catch (error) {
      console.log(error);
    }
    //setDeleteLoading(false)
  };

  const openViewFileModal = (file: IHealthRecord) => {
    setViewFileName(file.name);
    setViewFileUrl(file.url);
    setViewFileModal(true);
  };

  return (
    <Stack position="relative" direction="row" justifyContent="center" sx={{ width: "100%" }}>
      {upload && <UploadHealthRecordModal openUpload={true} close={addFileToTable} />}
      <Paper sx={{ mb: 2, width: "80%" }}>
        <EnhancedTableToolbar
          numSelected={selected.length}
          openModal={() => {
            setUpload(true);
          }}
        />
        <TableContainer>
          {/* <Box
            component="img"
            className="my-img"
            alt="The house from the offer."
            src={image}
            /> */}
          <Table sx={{ minWidth: 750 }} aria-labelledby="tableTitle" size={"medium"}>
            <TableHead sx={{ backgroundColor: "#103939", color: "white" }}>
              <TableRow>
                <TableCell sx={{ color: "white" }}>File Name</TableCell>
                <TableCell sx={{ color: "white" }} align="center">
                  Health Record Type
                </TableCell>
                <TableCell sx={{ color: "white" }} align="center">
                  File Type
                </TableCell>
                <TableCell sx={{ color: "white" }} align="center">
                  Upload Date
                </TableCell>
                <TableCell sx={{ color: "white" }} id="options" align="right"></TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {tableLoading ? (
                <TableLoadingSkeleton />
              ) : (
                files?.map((file, index) => (
                  <TableRow
                    role="checkbox"
                    hover
                    onClick={() => openViewFileModal(file)}
                    tabIndex={-1}
                    key={index}
                    sx={{
                      "&.Mui-selected, &.Mui-selected:hover": {
                        backgroundColor: "#1039394D", // Change this to your desired color
                        cursor: "pointer"
                      }
                    }}
                  >
                    <TableCell id={"enhanced-table-checkbox-" + index} scope="row">
                      {file.name}
                    </TableCell>
                    <TableCell align="center">{file.recordType}</TableCell>
                    <TableCell align="center">{file.fileType}</TableCell>
                    <TableCell align="center">{new Date(file.createdAt).toDateString()}</TableCell>
                    <TableCell>
                      <Stack
                        direction="row"
                        justifyContent="center"
                        divider={<Divider orientation="vertical" flexItem />}
                        spacing={2}
                      >
                        <IconButton
                          onClick={(e) => {
                            e.stopPropagation();
                            downloadFile(file.url, file.name);
                          }}
                        >
                          <DownloadIcon color="action" sx={{ color: "#103939" }} />
                        </IconButton>
                        <IconButton
                          onClick={(e) => {
                            e.stopPropagation();
                            deleteFile(file.url, index);
                          }}
                        >
                          {deleteLoading ? <CircularProgress size={24} /> : <DeleteIcon />}
                        </IconButton>
                      </Stack>
                    </TableCell>
                  </TableRow>
                ))
              )}
            </TableBody>
          </Table>
        </TableContainer>
      </Paper>
      <Modal
        open={viewFileModal}
        onClose={() => {
          setViewFileModal(false);
        }}
        aria-labelledby="modal-modal-title"
        aria-describedby="modal-modal-description"
      >
        <Box sx={FileViewModalStyle}>
          <Typography
            align="center"
            color="#103939"
            fontFamily="Inter"
            id="modal-modal-title"
            variant="h6"
            component="h2"
          >
            {viewFileName}
          </Typography>
          <iframe width="500px" height="400px" src={viewFileUrl}></iframe>
        </Box>
      </Modal>
    </Stack>
  );
};
export default MedicalHistory;
Follow-up Appointment Services
export const findFollowUpRequestForRegisteredPatientById = async (
  followUpAppointmentRequestId: string
) => {
  return await FollowUpAppointmentRequestForRegistered.findById(followUpAppointmentRequestId);
};

export const getFollowUpRequestsForRegisteredPatient = async (patientId: string) => {
  return await FollowUpAppointmentRequestForRegistered.find({
    patientId
  });
};

export const createFollowUpRequestForRegisteredPatient = async (
  request: IFollowUpAppointmentRequestForRegisteredPatient
) => {
  const followUpAppointmentRequest = new FollowUpAppointmentRequestForRegistered(request);
  await followUpAppointmentRequest.save();
};

export const rejectFollowUpRequestForRegisteredPatient = async (
  followUpAppointmentRequestId: string
) => {
  const followUpAppointmentRequest = await findFollowUpRequestForRegisteredPatientById(
    followUpAppointmentRequestId
  );
  if (!followUpAppointmentRequest) {
    throw new Error("Follow up appointment request not found");
  }
  followUpAppointmentRequest.status = "rejected";
  await followUpAppointmentRequest.save();
};

export const acceptFollowUpRequestForRegisteredPatient = async (
  followUpAppointmentRequestId: string,
  appointmentTimePeriod?: TimePeriod
) => {
  const followUpAppointmentRequest = await findFollowUpRequestForRegisteredPatientById(
    followUpAppointmentRequestId
  );
  if (!followUpAppointmentRequest) {
    throw new Error("Follow up appointment request not found");
  }
  await scheduleAFollowUpAppointment(
    followUpAppointmentRequest.doctorId.toString(),
    followUpAppointmentRequest.patientId.toString(),
    (followUpAppointmentRequest.timePeriod || appointmentTimePeriod)!
  );
  followUpAppointmentRequest.status = "accepted";
  await followUpAppointmentRequest.save();
};

const scheduleAFollowUpAppointment = async (
  doctorId: string,
  patientId: string,
  timePeriod: TimePeriod
) => {
  const startTime = timePeriod.startTime.toString();
  const endTime = timePeriod.endTime.toString();
  await validateAppointmentCreation(patientId, doctorId, startTime, endTime, UserRole.DOCTOR);

  const initialAppointment = await findMostRecentCompletedAppointment(doctorId, patientId);
  if (!initialAppointment || initialAppointment.status !== "completed") {
    throw new Error("No recent completed appointment found between the doctor and patient");
  }
  await saveAppointment(doctorId, patientId, startTime, endTime, true);
};
Patient Database Schema (Mongoose)
import mongoose, { Document, Schema } from "mongoose";
import isEmail from "validator/lib/isEmail";
import isMobileNumber from "validator/lib/isMobilePhone";
import { IPatient } from "./interfaces/IPatient";
import PasswordResetSchema from "../users/PasswordReset";
import WalletSchema from "../wallets/Wallet";
import bcrypt from "bcrypt";
import NotificationSchema from "../notifications/Notification";

enum Relation {
  WIFE = "wife",
  HUSBAND = "husband",
  CHILD = "children"
}

export interface IPatientModel extends IPatient, Document {}

export const PatientSchema = new Schema<IPatientModel>(
  {
    username: { type: String, required: true, unique: true, index: true },
    password: { type: String, required: true, select: false, bcrypt: true },
    email: {
      type: String,
      validate: [isEmail, "invalid email"],
      unique: true,
      index: true
    },
    name: { type: String, required: true },
    dateOfBirth: { type: Date, required: true },
    gender: { type: String, required: true, enum: ["male", "female"] },
    mobileNumber: { type: String, required: true },
    emergencyContact: {
      fullname: { type: String, required: true },
      mobileNumber: {
        type: String,
        required: true,
        validate: [isMobileNumber, "invalid mobile number"]
      },
      relationToPatient: { type: String, required: true }
    },

    deliveryAddresses: { type: Array<{ type: String }>, select: false },
    imageUrl: String,
    healthRecords: {
      type: Array<{
        type: {
          name: { type: String; required: true };
          url: { type: String; required: true };
          recordType: { type: String; required: true };
          fileType: { type: String; required: true };
          createdAt: { type: Date; immutable: true };
        };
      }>,
      required: false
    },
    subscribedPackage: {
      type: {
        packageId: {
          type: Schema.Types.ObjectId,
          ref: "HealthPackage",
          required: true
        },
        startDate: { type: Date, required: true },
        endDate: { type: Date, required: true },
        status: {
          type: String,
          enum: ["subscribed", "unsubscribed", "cancelled"],
          required: true
        }
      },
      required: false,
      select: false
    },
    dependentFamilyMembers: {
      type: [
        {
          name: { type: String, required: true },
          nationalId: { type: String, required: true, unique: true },
          dateOfBirth: { type: Date, required: true },
          gender: { type: String, enum: ["male", "female"], required: true },
          relation: {
            type: String,
            enum: Relation,
            required: true
          },
          subscribedPackage: {
            type: {
              packageId: {
                type: Schema.Types.ObjectId,
                ref: "HealthPackage",
                required: true
              },
              startDate: { type: Date, required: true },
              endDate: { type: Date, required: true },
              status: {
                type: ["subscribed", "unsubscribed", "cancelled"],
                required: true
              }
            },
            required: false
          },
          healthRecords: {
            type: Array<{
              type: {
                name: { type: String; required: true };
                url: { type: String; required: true };
                recordType: { type: String; required: true };
                fileType: { type: String; required: true };
                createdAt: { type: Date; immutable: true };
              };
            }>,
            required: false
          }
        }
      ],
      required: false,
      select: false
    },
    registeredFamilyMembers: {
      type: [
        {
          id: {
            type: Schema.Types.ObjectId,
            ref: "Patient",
            required: true,
            unique: true
          },
          relation: { type: String, enum: Relation, required: true }
        }
      ],
      required: false,
      select: false
    },
    registeredFamilyMemberRequests: {
      type: [
        {
          id: {
            type: Schema.Types.ObjectId,
            ref: "Patient",
            required: true,
            unique: true
          },
          relation: { type: String, enum: Relation, required: true }
        }
      ],
      required: false,
      select: false
    },
    wallet: {
      type: WalletSchema,
      select: false,
      required: false
    },
    passwordReset: {
      type: PasswordResetSchema,
      select: false
    },
    receivedNotifications: {
      type: Array<typeof NotificationSchema>,
      select: false,
      required: false
    }
  },
  { timestamps: true }
);

PatientSchema.plugin(require("mongoose-bcrypt"), { rounds: 10 });

PatientSchema.methods.verifyPasswordResetOtp = function (otp: string) {
  return bcrypt.compare(otp, this.passwordReset.otp);
};
PatientSchema.methods.verifyWalletPinCode = function (pinCode: string) {
  return bcrypt.compare(pinCode, this.wallet.pinCode);
};

PatientSchema.virtual("age").get(function () {
  let today = new Date();
  let birthDate: Date = this.dateOfBirth;
  let age = today.getFullYear() - birthDate.getFullYear();
  let monthDifference = today.getMonth() - birthDate.getMonth();

  if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  return age;
});

export default mongoose.model<IPatientModel>("Patient", PatientSchema);
export { Relation };

Installation

To install the project with npm, run the following commands in order.

> git clone https://github.com/advanced-computer-lab-2023/Code-of-Duty2-Clinic.git
> cd Code-of-Duty2-Clinic/
> cd server
> npm i
> cd ../client
> npm i
> cd ..

API reference

Please refer to the api-reference.md file under the docs directory for a comprehensive understanding of the API’s capabilities and usage guidelines.

Tests

Testing was done for this software using Postman.

Postman Postman Postman Postman Postman Postman Postman Postman Postman Postman Postman Postman Postman

How to Use

Please follow the following steps to run and use this software.

Set Environment Variables

To use this software, you must first add your environment variables. To do so, you must create two .env files, one directly inside the server/ folder and the other directly inside the client/ folder.

In each of those directories, you will find a .env.example file. You must copy its content and paste in your newly created .env files respectively. You must then fill in the values needed for each environment variable, with guidance on how to do so provided in each .env.example file.

Note: Some (and only some) environment variables can be ignored for the sake of ease of setup at the cost of the operation of one or two features, such as the Stripe API environment variables which will just prevent the credit card payment feature from working, however the rest of the system will work just fine.

Run the Backend

> cd server
> npm run dev

Run the Frontend

> cd client
> npm run dev

Access the client/server on the specified ports

The client and the server will be running on the ports specified in your .env files respectively.

Authors

From the Pharmacy Team

From the Clinic Team

Contribute

Contributions are always welcome!

See CONTRIBUTING.md for ways to get started. Please adhere to the Code of Conduct for this software outlined in said document.

Credits

Documentation

Tutorials

Websites and Tools

Courses

Articles

Books

Project Repositories

License

This project is licensed under the terms of the Apache 2.0 license. For more details, see the LICENSE file in the root of this repository.

About

Online Clinic Platform, part of a complete healthcare system also including a Pharmacy Platform.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages