├── .DS_Store ├── .env.example ├── .gitignore ├── README.md ├── Resources ├── PH HealthCare - Web Application Requirements.pdf └── PH HealthCare ERD.pdf ├── package.json ├── prisma ├── migrations │ ├── 20240312182853_init │ │ └── migration.sql │ ├── 20240312193216_updated │ │ └── migration.sql │ ├── 20240312193432_updated │ │ └── migration.sql │ ├── 20240315185623_status_added │ │ └── migration.sql │ ├── 20240320190409_doctor_model │ │ └── migration.sql │ ├── 20240320192304_patient │ │ └── migration.sql │ ├── 20240322175759_doctorspecialities │ │ └── migration.sql │ ├── 20240325173233_medical_report_and_health_data │ │ └── migration.sql │ ├── 20240325180933_update │ │ └── migration.sql │ ├── 20240325192249_schedule │ │ └── migration.sql │ ├── 20240325200800_update │ │ └── migration.sql │ ├── 20240327171042_set_default_value │ │ └── migration.sql │ ├── 20240328165142_appointment │ │ └── migration.sql │ ├── 20240328172550_updaterelation │ │ └── migration.sql │ ├── 20240328173851_payment │ │ └── migration.sql │ ├── 20240328174654_update │ │ └── migration.sql │ ├── 20240328190510_prescription │ │ └── migration.sql │ ├── 20240328191019_review │ │ └── migration.sql │ ├── 20240329181749_ │ │ └── migration.sql │ ├── 20240329181812_update │ │ └── migration.sql │ ├── 20240329182309_update_trx │ │ └── migration.sql │ ├── 20240330174216_add_avg_rating │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── src ├── app.ts ├── app │ ├── errors │ │ └── ApiError.ts │ ├── interfaces │ │ ├── common.ts │ │ ├── file.ts │ │ └── pagination.ts │ ├── middlewares │ │ ├── auth.ts │ │ ├── globalErrorHandler.ts │ │ └── validateRequest.ts │ ├── modules │ │ ├── Admin │ │ │ ├── admin.constant.ts │ │ │ ├── admin.controller.ts │ │ │ ├── admin.interface.ts │ │ │ ├── admin.routes.ts │ │ │ ├── admin.service.ts │ │ │ └── admin.validations.ts │ │ ├── Appointment │ │ │ ├── appointment.constant.ts │ │ │ ├── appointment.controller.ts │ │ │ ├── appointment.routes.ts │ │ │ ├── appointment.service.ts │ │ │ └── appointment.validation.ts │ │ ├── Auth │ │ │ ├── auth.controller.ts │ │ │ ├── auth.routes.ts │ │ │ ├── auth.service.ts │ │ │ └── emailSender.ts │ │ ├── Doctor │ │ │ ├── doctor.constants.ts │ │ │ ├── doctor.controller.ts │ │ │ ├── doctor.interface.ts │ │ │ ├── doctor.routes.ts │ │ │ ├── doctor.service.ts │ │ │ └── doctor.validation.ts │ │ ├── DoctorSchedule │ │ │ ├── doctorSchedule.constants.ts │ │ │ ├── doctorSchedule.controller.ts │ │ │ ├── doctorSchedule.interface.ts │ │ │ ├── doctorSchedule.routes.ts │ │ │ ├── doctorSchedule.service.ts │ │ │ └── doctorSchedule.validation.ts │ │ ├── Meta │ │ │ ├── meta.controller.ts │ │ │ ├── meta.routes.ts │ │ │ └── meta.service.ts │ │ ├── Patient │ │ │ ├── patient.constants.ts │ │ │ ├── patient.controller.ts │ │ │ ├── patient.interface.ts │ │ │ ├── patient.route.ts │ │ │ ├── patient.services.ts │ │ │ └── patient.validation.ts │ │ ├── Payment │ │ │ ├── payment.controller.ts │ │ │ ├── payment.routes.ts │ │ │ └── payment.service.ts │ │ ├── Prescription │ │ │ ├── prescription.constants.ts │ │ │ ├── prescription.controller.ts │ │ │ ├── prescription.routes.ts │ │ │ ├── prescription.service.ts │ │ │ └── prescription.validation.ts │ │ ├── Review │ │ │ ├── review.contant.ts │ │ │ ├── review.controller.ts │ │ │ ├── review.routes.ts │ │ │ ├── review.service.ts │ │ │ └── review.validation.ts │ │ ├── SSL │ │ │ ├── ssl.interface.ts │ │ │ └── ssl.service.ts │ │ ├── Schedule │ │ │ ├── schedule.controller.ts │ │ │ ├── schedule.interface.ts │ │ │ ├── schedule.routes.ts │ │ │ └── schedule.sevice.ts │ │ ├── Specialties │ │ │ ├── specialties.controller.ts │ │ │ ├── specialties.routes.ts │ │ │ ├── specialties.service.ts │ │ │ └── specialties.validation.ts │ │ └── User │ │ │ ├── user.constant.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.routes.ts │ │ │ ├── user.sevice.ts │ │ │ └── user.validation.ts │ └── routes │ │ └── index.ts ├── config │ └── index.ts ├── helpars │ ├── fileUploader.ts │ ├── jwtHelpers.ts │ └── paginationHelper.ts ├── server.ts └── shared │ ├── catchAsync.ts │ ├── pick.ts │ ├── prisma.ts │ └── sendResponse.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo-Level2-Web-Dev/PH-HealthCare-Server/38d78af6aef6aeb2dd454e3866c3f449012337e2/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | PORT=3000 3 | DATABASE_URL="postgresql://DB_USER:DB_PASS@localhost:5432/DB_NAME?schema=public" 4 | JWT_SECRET="YOUR SECRET" 5 | EXPIRES_IN="30d" 6 | REFRESH_TOKEN_SECRET="YOUR SECRET" 7 | REFRESH_TOKEN_EXPIRES_IN="30d" 8 | RESET_PASS_TOKEN="YOUR TOKEN SECRET" 9 | RESET_PASS_TOKEN_EXPIRES_IN="5m" 10 | RESET_PASS_LINK="FRONT-END RESET PASSWORD FORM LINK" 11 | EMAIL = "YOUR EMAIL" 12 | APP_PASS = "YOUR APP PASSWORD" 13 | STORE_ID = "SSL STORE ID" 14 | STORE_PASS = "SSL STORE PASSWORD" 15 | SUCCESS_URL = "http://localhost:3030/success" 16 | CANCEL_URL = "http://localhost:3030/cancel" 17 | FAIL_URL = "http://localhost:3030/fail" 18 | SSL_PAYMENT_API = "PAYMENT API" 19 | SSL_VALIDATIOIN_API = "PAYMENT VALIDATION API" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PH HealthCare Server 2 | 3 | Welcome to the backend repository for PH HealthCare, a tutorial project developed as part of the "Level 2 Web Development Course" offered by Programming Hero. 4 | 5 | This repository contains the backend codebase responsible for handling server-side logic, database management, and communication between different system components. 6 | 7 | 17 | 18 | 27 | 28 | 35 | 36 | ## Installation and Setup 37 | 1. Clone this repository: `git clone ` 38 | 2. Install dependencies: `npm install` 39 | 3. Set up the environment variables by creating a `.env` file and filling in the required variables based on the provided `.env.example` file. 40 | 4. Run the database migrations: `npx prisma migrate dev` 41 | 4. Run the Command: `npm run seed` 42 | 5. Start the server: `npm run dev` 43 | 44 | 45 | ### API Documentation: [POSTMAN API DOCUMENTATION (Click Here)](https://documenter.getpostman.com/view/26694209/2sA2xjyWRv) 46 | 47 | 48 | -------------------------------------------------------------------------------- /Resources/PH HealthCare - Web Application Requirements.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo-Level2-Web-Dev/PH-HealthCare-Server/38d78af6aef6aeb2dd454e3866c3f449012337e2/Resources/PH HealthCare - Web Application Requirements.pdf -------------------------------------------------------------------------------- /Resources/PH HealthCare ERD.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo-Level2-Web-Dev/PH-HealthCare-Server/38d78af6aef6aeb2dd454e3866c3f449012337e2/Resources/PH HealthCare ERD.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PH-HealthCare-Server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Apollo-Level2-Web-Dev/PH-HealthCare-Server.git", 6 | "author": "fahimahammed ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "ts-node-dev --respawn --transpile-only src/server.ts", 10 | "seed": "ts-node-dev --respawn --transpile-only prisma/seed.ts" 11 | }, 12 | "devDependencies": { 13 | "@types/bcrypt": "^5.0.2", 14 | "@types/cors": "^2.8.17", 15 | "@types/express": "^4.17.21", 16 | "@types/jsonwebtoken": "^9.0.6", 17 | "@types/luxon": "^3.4.2", 18 | "@types/multer": "^1.4.11", 19 | "@types/node": "^20.11.26", 20 | "@types/node-cron": "^3.0.11", 21 | "@types/nodemailer": "^6.4.14", 22 | "@types/uuid": "^9.0.8", 23 | "prisma": "^5.11.0", 24 | "ts-node": "^10.9.2", 25 | "ts-node-dev": "^2.0.0", 26 | "typescript": "^5.4.2" 27 | }, 28 | "dependencies": { 29 | "@prisma/client": "5.11.0", 30 | "@types/cookie-parser": "^1.4.7", 31 | "axios": "^1.6.8", 32 | "bcrypt": "^5.1.1", 33 | "cloudinary": "^2.0.3", 34 | "cookie-parser": "^1.4.6", 35 | "cors": "^2.8.5", 36 | "date-fns": "^3.6.0", 37 | "dotenv": "^16.4.5", 38 | "express": "^4.18.3", 39 | "http-status": "^1.7.4", 40 | "jsonwebtoken": "^9.0.2", 41 | "luxon": "^3.4.4", 42 | "multer": "^1.4.5-lts.1", 43 | "node-cron": "^3.0.3", 44 | "nodemailer": "^6.9.12", 45 | "sslcommerz-lts": "^1.1.0", 46 | "uuid": "^9.0.1", 47 | "zod": "^3.22.4" 48 | } 49 | } -------------------------------------------------------------------------------- /prisma/migrations/20240312182853_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "UserRole" AS ENUM ('SUPER_ADMIN', 'ADMIN', 'DOCTOR', 'PATIENT'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "UserStatus" AS ENUM ('ACTIVE', 'BLOCKED'); 6 | 7 | -- CreateTable 8 | CREATE TABLE "users" ( 9 | "id" TEXT NOT NULL, 10 | "email" TEXT NOT NULL, 11 | "password" TEXT NOT NULL, 12 | "role" "UserRole" NOT NULL, 13 | "needPasswordChange" BOOLEAN NOT NULL, 14 | "status" "UserStatus" NOT NULL, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP(3) NOT NULL, 17 | 18 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateTable 22 | CREATE TABLE "admins" ( 23 | "id" TEXT NOT NULL, 24 | "name" TEXT NOT NULL, 25 | "email" TEXT NOT NULL, 26 | "profilePhoto" TEXT, 27 | "contactNumber" TEXT NOT NULL, 28 | "isDeleted" BOOLEAN NOT NULL, 29 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | "updatedAt" TIMESTAMP(3) NOT NULL, 31 | 32 | CONSTRAINT "admins_pkey" PRIMARY KEY ("id") 33 | ); 34 | 35 | -- CreateIndex 36 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 37 | 38 | -- CreateIndex 39 | CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email"); 40 | 41 | -- AddForeignKey 42 | ALTER TABLE "admins" ADD CONSTRAINT "admins_email_fkey" FOREIGN KEY ("email") REFERENCES "users"("email") ON DELETE RESTRICT ON UPDATE CASCADE; 43 | -------------------------------------------------------------------------------- /prisma/migrations/20240312193216_updated/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ALTER COLUMN "needPasswordChange" SET DEFAULT true, 3 | ALTER COLUMN "status" SET DEFAULT 'ACTIVE'; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240312193432_updated/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "admins" ALTER COLUMN "isDeleted" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240315185623_status_added/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "UserStatus" ADD VALUE 'DELETED'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240320190409_doctor_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Gender" AS ENUM ('MALE', 'FEMALE'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "doctors" ( 6 | "id" TEXT NOT NULL, 7 | "name" TEXT NOT NULL, 8 | "email" TEXT NOT NULL, 9 | "profilePhoto" TEXT, 10 | "contactNumber" TEXT NOT NULL, 11 | "address" TEXT, 12 | "registrationNumber" TEXT NOT NULL, 13 | "experience" INTEGER NOT NULL DEFAULT 0, 14 | "gender" "Gender" NOT NULL, 15 | "appointmentFee" INTEGER NOT NULL, 16 | "qualification" TEXT NOT NULL, 17 | "currentWorkingPlace" TEXT NOT NULL, 18 | "designation" TEXT NOT NULL, 19 | "isDeleted" BOOLEAN NOT NULL DEFAULT false, 20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" TIMESTAMP(3) NOT NULL, 22 | 23 | CONSTRAINT "doctors_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- CreateIndex 27 | CREATE UNIQUE INDEX "doctors_email_key" ON "doctors"("email"); 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "doctors" ADD CONSTRAINT "doctors_email_fkey" FOREIGN KEY ("email") REFERENCES "users"("email") ON DELETE RESTRICT ON UPDATE CASCADE; 31 | -------------------------------------------------------------------------------- /prisma/migrations/20240320192304_patient/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "patients" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "name" TEXT NOT NULL, 6 | "profilePhoto" TEXT, 7 | "contactNumber" TEXT, 8 | "address" TEXT, 9 | "isDeleted" BOOLEAN NOT NULL DEFAULT false, 10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" TIMESTAMP(3) NOT NULL, 12 | 13 | CONSTRAINT "patients_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "patients_id_key" ON "patients"("id"); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "patients_email_key" ON "patients"("email"); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "patients" ADD CONSTRAINT "patients_email_fkey" FOREIGN KEY ("email") REFERENCES "users"("email") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20240322175759_doctorspecialities/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "specialties" ( 3 | "id" TEXT NOT NULL, 4 | "title" TEXT NOT NULL, 5 | "icon" TEXT NOT NULL, 6 | 7 | CONSTRAINT "specialties_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "doctor_specialties" ( 12 | "specialitiesId" TEXT NOT NULL, 13 | "doctorId" TEXT NOT NULL, 14 | 15 | CONSTRAINT "doctor_specialties_pkey" PRIMARY KEY ("specialitiesId","doctorId") 16 | ); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "doctor_specialties" ADD CONSTRAINT "doctor_specialties_specialitiesId_fkey" FOREIGN KEY ("specialitiesId") REFERENCES "specialties"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "doctor_specialties" ADD CONSTRAINT "doctor_specialties_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "doctors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240325173233_medical_report_and_health_data/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "BloodGroup" AS ENUM ('A_POSITIVE', 'B_POSITIVE', 'O_POSITIVE', 'AB_POSITIVE', 'A_NEGATIVE', 'B_NEGATIVE', 'O_NEGATIVE', 'AB_NEGATIVE'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "MaritalStatus" AS ENUM ('MARRIED', 'UNMARRIED'); 6 | 7 | -- CreateTable 8 | CREATE TABLE "patient_health_datas" ( 9 | "id" TEXT NOT NULL, 10 | "patientId" TEXT NOT NULL, 11 | "gender" "Gender" NOT NULL, 12 | "dateOfBirth" TEXT NOT NULL, 13 | "bloodGroup" "BloodGroup" NOT NULL, 14 | "hasAllergies" BOOLEAN NOT NULL, 15 | "hasDiabetes" BOOLEAN NOT NULL, 16 | "height" TEXT NOT NULL, 17 | "weight" TEXT NOT NULL, 18 | "smokingStatus" BOOLEAN NOT NULL, 19 | "dietaryPreferences" TEXT NOT NULL, 20 | "pregnancyStatus" BOOLEAN NOT NULL, 21 | "mentalHealthHistory" TEXT NOT NULL, 22 | "immunizationStatus" TEXT NOT NULL, 23 | "hasPastSurgeries" BOOLEAN NOT NULL, 24 | "recentAnxiety" BOOLEAN NOT NULL, 25 | "recentDepression" BOOLEAN NOT NULL, 26 | "maritalStatus" "MaritalStatus" NOT NULL, 27 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 28 | "updatedAt" TIMESTAMP(3) NOT NULL, 29 | 30 | CONSTRAINT "patient_health_datas_pkey" PRIMARY KEY ("id") 31 | ); 32 | 33 | -- CreateTable 34 | CREATE TABLE "madical_reports" ( 35 | "id" TEXT NOT NULL, 36 | "patientId" TEXT NOT NULL, 37 | "reportName" TEXT NOT NULL, 38 | "reportLink" TEXT NOT NULL, 39 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 40 | "updatedAt" TIMESTAMP(3) NOT NULL, 41 | 42 | CONSTRAINT "madical_reports_pkey" PRIMARY KEY ("id") 43 | ); 44 | 45 | -- CreateIndex 46 | CREATE UNIQUE INDEX "patient_health_datas_patientId_key" ON "patient_health_datas"("patientId"); 47 | 48 | -- AddForeignKey 49 | ALTER TABLE "patient_health_datas" ADD CONSTRAINT "patient_health_datas_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 50 | 51 | -- AddForeignKey 52 | ALTER TABLE "madical_reports" ADD CONSTRAINT "madical_reports_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 53 | -------------------------------------------------------------------------------- /prisma/migrations/20240325180933_update/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "patient_health_datas" ALTER COLUMN "hasAllergies" DROP NOT NULL, 3 | ALTER COLUMN "hasAllergies" SET DEFAULT false, 4 | ALTER COLUMN "hasDiabetes" DROP NOT NULL, 5 | ALTER COLUMN "hasDiabetes" SET DEFAULT false, 6 | ALTER COLUMN "smokingStatus" DROP NOT NULL, 7 | ALTER COLUMN "smokingStatus" SET DEFAULT false, 8 | ALTER COLUMN "dietaryPreferences" DROP NOT NULL, 9 | ALTER COLUMN "pregnancyStatus" DROP NOT NULL, 10 | ALTER COLUMN "pregnancyStatus" SET DEFAULT false, 11 | ALTER COLUMN "mentalHealthHistory" DROP NOT NULL, 12 | ALTER COLUMN "immunizationStatus" DROP NOT NULL, 13 | ALTER COLUMN "hasPastSurgeries" DROP NOT NULL, 14 | ALTER COLUMN "hasPastSurgeries" SET DEFAULT false, 15 | ALTER COLUMN "recentAnxiety" DROP NOT NULL, 16 | ALTER COLUMN "recentAnxiety" SET DEFAULT false, 17 | ALTER COLUMN "recentDepression" DROP NOT NULL, 18 | ALTER COLUMN "recentDepression" SET DEFAULT false, 19 | ALTER COLUMN "maritalStatus" SET DEFAULT 'UNMARRIED'; 20 | -------------------------------------------------------------------------------- /prisma/migrations/20240325192249_schedule/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "schedules" ( 3 | "id" TEXT NOT NULL, 4 | "startDate" TIMESTAMP(3) NOT NULL, 5 | "endDate" TIMESTAMP(3) NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | 9 | CONSTRAINT "schedules_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "doctor_schedules" ( 14 | "doctorId" TEXT NOT NULL, 15 | "scheduleId" TEXT NOT NULL, 16 | "isBooked" BOOLEAN NOT NULL, 17 | "appointmentId" TEXT, 18 | 19 | CONSTRAINT "doctor_schedules_pkey" PRIMARY KEY ("doctorId","scheduleId") 20 | ); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "doctor_schedules" ADD CONSTRAINT "doctor_schedules_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "doctors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "doctor_schedules" ADD CONSTRAINT "doctor_schedules_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "schedules"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /prisma/migrations/20240325200800_update/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `endDate` on the `schedules` table. All the data in the column will be lost. 5 | - You are about to drop the column `startDate` on the `schedules` table. All the data in the column will be lost. 6 | - Added the required column `endDateTime` to the `schedules` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `startDateTime` to the `schedules` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "schedules" DROP COLUMN "endDate", 12 | DROP COLUMN "startDate", 13 | ADD COLUMN "endDateTime" TIMESTAMP(3) NOT NULL, 14 | ADD COLUMN "startDateTime" TIMESTAMP(3) NOT NULL; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240327171042_set_default_value/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "doctor_schedules" ALTER COLUMN "isBooked" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240328165142_appointment/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "AppointmentStatus" AS ENUM ('SCHEDULED', 'INPROGRESS', 'COMPLETED', 'CANCELED'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "PaymentStatus" AS ENUM ('PAID', 'UNPAID'); 6 | 7 | -- CreateTable 8 | CREATE TABLE "appointments" ( 9 | "id" TEXT NOT NULL, 10 | "patientId" TEXT NOT NULL, 11 | "doctorId" TEXT NOT NULL, 12 | "scheduleId" TEXT NOT NULL, 13 | "videoCallingId" TEXT NOT NULL, 14 | "status" "AppointmentStatus" NOT NULL DEFAULT 'SCHEDULED', 15 | "paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'UNPAID', 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | 19 | CONSTRAINT "appointments_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "appointments_scheduleId_key" ON "appointments"("scheduleId"); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "appointments" ADD CONSTRAINT "appointments_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "appointments" ADD CONSTRAINT "appointments_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "doctors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "appointments" ADD CONSTRAINT "appointments_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "schedules"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 33 | -------------------------------------------------------------------------------- /prisma/migrations/20240328172550_updaterelation/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[appointmentId]` on the table `doctor_schedules` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "doctor_schedules_appointmentId_key" ON "doctor_schedules"("appointmentId"); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "doctor_schedules" ADD CONSTRAINT "doctor_schedules_appointmentId_fkey" FOREIGN KEY ("appointmentId") REFERENCES "appointments"("id") ON DELETE SET NULL ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240328173851_payment/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "payments" ( 3 | "id" TEXT NOT NULL, 4 | "appointmentId" TEXT NOT NULL, 5 | "amount" DOUBLE PRECISION NOT NULL, 6 | "transactionId" TEXT NOT NULL, 7 | "status" "PaymentStatus" NOT NULL, 8 | "paymentGatewayData" JSONB, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" TIMESTAMP(3) NOT NULL, 11 | 12 | CONSTRAINT "payments_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "payments_appointmentId_key" ON "payments"("appointmentId"); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "payments" ADD CONSTRAINT "payments_appointmentId_fkey" FOREIGN KEY ("appointmentId") REFERENCES "appointments"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 20 | -------------------------------------------------------------------------------- /prisma/migrations/20240328174654_update/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "payments" ALTER COLUMN "status" SET DEFAULT 'UNPAID'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240328190510_prescription/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "prescriptions" ( 3 | "id" TEXT NOT NULL, 4 | "appointmentId" TEXT NOT NULL, 5 | "doctorId" TEXT NOT NULL, 6 | "patientId" TEXT NOT NULL, 7 | "instructions" TEXT NOT NULL, 8 | "followUpDate" TIMESTAMP(3), 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" TIMESTAMP(3) NOT NULL, 11 | 12 | CONSTRAINT "prescriptions_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "prescriptions_appointmentId_key" ON "prescriptions"("appointmentId"); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "prescriptions" ADD CONSTRAINT "prescriptions_appointmentId_fkey" FOREIGN KEY ("appointmentId") REFERENCES "appointments"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "prescriptions" ADD CONSTRAINT "prescriptions_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "doctors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "prescriptions" ADD CONSTRAINT "prescriptions_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20240328191019_review/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "reviews" ( 3 | "id" TEXT NOT NULL, 4 | "patientId" TEXT NOT NULL, 5 | "doctorId" TEXT NOT NULL, 6 | "appointmentId" TEXT NOT NULL, 7 | "rating" DOUBLE PRECISION NOT NULL, 8 | "comment" TEXT NOT NULL, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" TIMESTAMP(3) NOT NULL, 11 | 12 | CONSTRAINT "reviews_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "reviews_appointmentId_key" ON "reviews"("appointmentId"); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "reviews" ADD CONSTRAINT "reviews_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "patients"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "reviews" ADD CONSTRAINT "reviews_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "doctors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "reviews" ADD CONSTRAINT "reviews_appointmentId_fkey" FOREIGN KEY ("appointmentId") REFERENCES "appointments"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20240329181749_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[transactionId]` on the table `payments` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "payments_transactionId_key" ON "payments"("transactionId"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240329181812_update/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "payments_transactionId_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240329182309_update_trx/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[transactionId]` on the table `payments` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "payments_transactionId_key" ON "payments"("transactionId"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240330174216_add_avg_rating/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "doctors" ADD COLUMN "averageRating" DOUBLE PRECISION NOT NULL DEFAULT 0.0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id @default(uuid()) 18 | email String @unique 19 | password String 20 | role UserRole 21 | needPasswordChange Boolean @default(true) 22 | status UserStatus @default(ACTIVE) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | admin Admin? 26 | doctor Doctor? 27 | patient Patient? 28 | 29 | @@map("users") 30 | } 31 | 32 | model Admin { 33 | id String @id @default(uuid()) 34 | name String 35 | email String @unique 36 | profilePhoto String? 37 | contactNumber String 38 | isDeleted Boolean @default(false) 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | 42 | user User @relation(fields: [email], references: [email]) 43 | 44 | @@map("admins") 45 | } 46 | 47 | model Doctor { 48 | id String @id @default(uuid()) 49 | name String 50 | email String @unique 51 | profilePhoto String? 52 | contactNumber String 53 | address String? 54 | registrationNumber String 55 | experience Int @default(0) 56 | gender Gender 57 | appointmentFee Int 58 | qualification String 59 | currentWorkingPlace String 60 | designation String 61 | isDeleted Boolean @default(false) 62 | averageRating Float @default(0.0) 63 | createdAt DateTime @default(now()) 64 | updatedAt DateTime @updatedAt 65 | user User @relation(fields: [email], references: [email]) 66 | doctorSpecialties DoctorSpecialties[] 67 | doctorSchedules DoctorSchedules[] 68 | appointment Appointment[] 69 | prescription Prescription[] 70 | review Review[] 71 | 72 | @@map("doctors") 73 | } 74 | 75 | model Patient { 76 | id String @id @unique @default(uuid()) 77 | email String @unique 78 | name String 79 | profilePhoto String? 80 | contactNumber String? 81 | address String? 82 | isDeleted Boolean @default(false) 83 | createdAt DateTime @default(now()) 84 | updatedAt DateTime @updatedAt 85 | 86 | user User @relation(references: [email], fields: [email]) 87 | patientHealthData PatientHealthData? 88 | medicalReport MedicalReport[] 89 | appointment Appointment[] 90 | prescription Prescription[] 91 | review Review[] 92 | 93 | @@map("patients") 94 | } 95 | 96 | model Specialties { 97 | id String @id @default(uuid()) 98 | title String 99 | icon String 100 | doctorSpecialties DoctorSpecialties[] 101 | 102 | @@map("specialties") 103 | } 104 | 105 | model DoctorSpecialties { 106 | specialitiesId String 107 | specialities Specialties @relation(fields: [specialitiesId], references: [id]) 108 | 109 | doctorId String 110 | doctor Doctor @relation(fields: [doctorId], references: [id]) 111 | 112 | @@id([specialitiesId, doctorId]) 113 | @@map("doctor_specialties") 114 | } 115 | 116 | model PatientHealthData { 117 | id String @id @default(uuid()) 118 | patientId String @unique 119 | patient Patient @relation(fields: [patientId], references: [id]) 120 | gender Gender 121 | dateOfBirth String 122 | bloodGroup BloodGroup 123 | hasAllergies Boolean? @default(false) 124 | hasDiabetes Boolean? @default(false) 125 | height String 126 | weight String 127 | smokingStatus Boolean? @default(false) 128 | dietaryPreferences String? 129 | pregnancyStatus Boolean? @default(false) 130 | mentalHealthHistory String? 131 | immunizationStatus String? 132 | hasPastSurgeries Boolean? @default(false) 133 | recentAnxiety Boolean? @default(false) 134 | recentDepression Boolean? @default(false) 135 | maritalStatus MaritalStatus @default(UNMARRIED) 136 | createdAt DateTime @default(now()) 137 | updatedAt DateTime @updatedAt 138 | 139 | @@map("patient_health_datas") 140 | } 141 | 142 | model MedicalReport { 143 | id String @id @default(uuid()) 144 | patientId String 145 | patient Patient @relation(fields: [patientId], references: [id]) 146 | reportName String 147 | reportLink String 148 | createdAt DateTime @default(now()) 149 | updatedAt DateTime @updatedAt 150 | 151 | @@map("madical_reports") 152 | } 153 | 154 | model Schedule { 155 | id String @id @default(uuid()) 156 | startDateTime DateTime 157 | endDateTime DateTime 158 | createdAt DateTime @default(now()) 159 | updatedAt DateTime @updatedAt 160 | doctorSchedules DoctorSchedules[] 161 | appointment Appointment? 162 | 163 | @@map("schedules") 164 | } 165 | 166 | model DoctorSchedules { 167 | doctorId String 168 | doctor Doctor @relation(fields: [doctorId], references: [id]) 169 | 170 | scheduleId String 171 | schedule Schedule @relation(fields: [scheduleId], references: [id]) 172 | 173 | isBooked Boolean @default(false) 174 | 175 | appointmentId String? @unique 176 | appointment Appointment? @relation(fields: [appointmentId], references: [id]) 177 | 178 | @@id([doctorId, scheduleId]) 179 | @@map("doctor_schedules") 180 | } 181 | 182 | model Appointment { 183 | id String @id @default(uuid()) 184 | patientId String 185 | patient Patient @relation(fields: [patientId], references: [id]) 186 | 187 | doctorId String 188 | doctor Doctor @relation(fields: [doctorId], references: [id]) 189 | 190 | scheduleId String @unique 191 | schedule Schedule @relation(fields: [scheduleId], references: [id]) 192 | 193 | videoCallingId String 194 | status AppointmentStatus @default(SCHEDULED) 195 | paymentStatus PaymentStatus @default(UNPAID) 196 | createdAt DateTime @default(now()) 197 | updatedAt DateTime @updatedAt 198 | doctorSchedules DoctorSchedules? 199 | payment Payment? 200 | prescription Prescription? 201 | review Review? 202 | 203 | @@map("appointments") 204 | } 205 | 206 | model Payment { 207 | id String @id @default(uuid()) 208 | appointmentId String @unique 209 | appointment Appointment @relation(fields: [appointmentId], references: [id]) 210 | 211 | amount Float 212 | transactionId String @unique 213 | status PaymentStatus @default(UNPAID) 214 | paymentGatewayData Json? 215 | createdAt DateTime @default(now()) 216 | updatedAt DateTime @updatedAt 217 | 218 | @@map("payments") 219 | } 220 | 221 | model Prescription { 222 | id String @id @default(uuid()) 223 | appointmentId String @unique 224 | appointment Appointment @relation(fields: [appointmentId], references: [id]) 225 | 226 | doctorId String 227 | doctor Doctor @relation(fields: [doctorId], references: [id]) 228 | 229 | patientId String 230 | patient Patient @relation(fields: [patientId], references: [id]) 231 | 232 | instructions String 233 | followUpDate DateTime? 234 | createdAt DateTime @default(now()) 235 | updatedAt DateTime @updatedAt 236 | 237 | @@map("prescriptions") 238 | } 239 | 240 | model Review { 241 | id String @id @default(uuid()) 242 | patientId String 243 | patient Patient @relation(fields: [patientId], references: [id]) 244 | 245 | doctorId String 246 | doctor Doctor @relation(fields: [doctorId], references: [id]) 247 | 248 | appointmentId String @unique 249 | appointment Appointment @relation(fields: [appointmentId], references: [id]) 250 | 251 | rating Float 252 | comment String 253 | createdAt DateTime @default(now()) 254 | updatedAt DateTime @updatedAt 255 | 256 | @@map("reviews") 257 | } 258 | 259 | enum UserRole { 260 | SUPER_ADMIN 261 | ADMIN 262 | DOCTOR 263 | PATIENT 264 | } 265 | 266 | enum UserStatus { 267 | ACTIVE 268 | BLOCKED 269 | DELETED 270 | } 271 | 272 | enum Gender { 273 | MALE 274 | FEMALE 275 | } 276 | 277 | enum BloodGroup { 278 | A_POSITIVE 279 | B_POSITIVE 280 | O_POSITIVE 281 | AB_POSITIVE 282 | A_NEGATIVE 283 | B_NEGATIVE 284 | O_NEGATIVE 285 | AB_NEGATIVE 286 | } 287 | 288 | enum MaritalStatus { 289 | MARRIED 290 | UNMARRIED 291 | } 292 | 293 | enum AppointmentStatus { 294 | SCHEDULED 295 | INPROGRESS 296 | COMPLETED 297 | CANCELED 298 | } 299 | 300 | enum PaymentStatus { 301 | PAID 302 | UNPAID 303 | } 304 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@prisma/client"; 2 | import prisma from "../src/shared/prisma"; 3 | import * as bcrypt from 'bcrypt' 4 | 5 | const seedSuperAdmin = async () => { 6 | try { 7 | const isExistSuperAdmin = await prisma.user.findFirst({ 8 | where: { 9 | role: UserRole.SUPER_ADMIN 10 | } 11 | }); 12 | 13 | if (isExistSuperAdmin) { 14 | console.log("Super admin already exists!") 15 | return; 16 | }; 17 | 18 | const hashedPassword = await bcrypt.hash("superadmin", 12) 19 | 20 | const superAdminData = await prisma.user.create({ 21 | data: { 22 | email: "super@admin.com", 23 | password: hashedPassword, 24 | role: UserRole.SUPER_ADMIN, 25 | admin: { 26 | create: { 27 | name: "Super Admin", 28 | //email: "super@admin.com", 29 | contactNumber: "01234567890" 30 | } 31 | } 32 | } 33 | }); 34 | 35 | console.log("Super Admin Created Successfully!", superAdminData); 36 | } 37 | catch (err) { 38 | console.error(err); 39 | } 40 | finally { 41 | await prisma.$disconnect(); 42 | } 43 | }; 44 | 45 | seedSuperAdmin(); -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { Application, NextFunction, Request, Response } from 'express'; 2 | import cors from 'cors'; 3 | import router from './app/routes'; 4 | import httpStatus from 'http-status'; 5 | import globalErrorHandler from './app/middlewares/globalErrorHandler'; 6 | import cookieParser from 'cookie-parser'; 7 | import { AppointmentService } from './app/modules/Appointment/appointment.service'; 8 | import cron from 'node-cron' 9 | 10 | const app: Application = express(); 11 | app.use(cors()); 12 | app.use(cookieParser()); 13 | 14 | //parser 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: true })); 17 | 18 | 19 | 20 | cron.schedule('* * * * *', () => { 21 | try { 22 | AppointmentService.cancelUnpaidAppointments(); 23 | } 24 | catch (err) { 25 | console.error(err); 26 | } 27 | }); 28 | 29 | app.get('/', (req: Request, res: Response) => { 30 | res.send({ 31 | Message: "Ph health care server.." 32 | }) 33 | }); 34 | 35 | app.use('/api/v1', router); 36 | 37 | app.use(globalErrorHandler); 38 | 39 | app.use((req: Request, res: Response, next: NextFunction) => { 40 | res.status(httpStatus.NOT_FOUND).json({ 41 | success: false, 42 | message: "API NOT FOUND!", 43 | error: { 44 | path: req.originalUrl, 45 | message: "Your requested path is not found!" 46 | } 47 | }) 48 | }) 49 | 50 | export default app; -------------------------------------------------------------------------------- /src/app/errors/ApiError.ts: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | statusCode: number; 3 | constructor(statusCode: number, message: string | undefined, stack = '') { 4 | super(message) 5 | this.statusCode = statusCode 6 | if (stack) { 7 | this.stack = stack 8 | } 9 | else { 10 | Error.captureStackTrace(this, this.constructor); 11 | } 12 | } 13 | } 14 | 15 | export default ApiError; -------------------------------------------------------------------------------- /src/app/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@prisma/client"; 2 | 3 | export type IAuthUser = { 4 | email: string; 5 | role: UserRole 6 | } | null; -------------------------------------------------------------------------------- /src/app/interfaces/file.ts: -------------------------------------------------------------------------------- 1 | export type ICloudinaryResponse = { 2 | asset_id: string; 3 | public_id: string 4 | version: number; 5 | version_id: string 6 | signature: string 7 | width: number 8 | height: number 9 | format: string 10 | resource_type: string 11 | created_at: string 12 | tags: string[] 13 | bytes: number 14 | type: string 15 | etag: string 16 | placeholder: boolean 17 | url: string 18 | secure_url: string 19 | folder: string 20 | overwritten: boolean 21 | original_filename: string 22 | original_extension: string 23 | api_key: string 24 | } 25 | 26 | export type IFile = { 27 | fieldname: string 28 | originalname: string 29 | encoding: string 30 | mimetype: string 31 | destination: string 32 | filename: string 33 | path: string 34 | size: number 35 | } -------------------------------------------------------------------------------- /src/app/interfaces/pagination.ts: -------------------------------------------------------------------------------- 1 | export type IPaginationOptions = { 2 | page?: number; 3 | limit?: number; 4 | sortBy?: string | undefined; 5 | sortOrder?: string | undefined; 6 | } -------------------------------------------------------------------------------- /src/app/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { jwtHelpers } from "../../helpars/jwtHelpers"; 3 | import config from "../../config"; 4 | import { Secret } from "jsonwebtoken"; 5 | import ApiError from "../errors/ApiError"; 6 | import httpStatus from "http-status"; 7 | 8 | 9 | const auth = (...roles: string[]) => { 10 | return async (req: Request & { user?: any }, res: Response, next: NextFunction) => { 11 | try { 12 | const token = req.headers.authorization 13 | 14 | if (!token) { 15 | throw new ApiError(httpStatus.UNAUTHORIZED, "You are not authorized!") 16 | } 17 | 18 | const verifiedUser = jwtHelpers.verifyToken(token, config.jwt.jwt_secret as Secret) 19 | 20 | req.user = verifiedUser; 21 | 22 | if (roles.length && !roles.includes(verifiedUser.role)) { 23 | throw new ApiError(httpStatus.FORBIDDEN, "Forbidden!") 24 | } 25 | next() 26 | } 27 | catch (err) { 28 | next(err) 29 | } 30 | } 31 | }; 32 | 33 | export default auth; -------------------------------------------------------------------------------- /src/app/middlewares/globalErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { NextFunction, Request, Response } from "express" 3 | import httpStatus from "http-status" 4 | 5 | const globalErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { 6 | 7 | let statusCode = httpStatus.INTERNAL_SERVER_ERROR; 8 | let success = false; 9 | let message = err.message || "Something went wrong!"; 10 | let error = err; 11 | 12 | if (err instanceof Prisma.PrismaClientValidationError) { 13 | message = 'Validation Error'; 14 | error = err.message 15 | } 16 | else if (err instanceof Prisma.PrismaClientKnownRequestError) { 17 | if (err.code === 'P2002') { 18 | message = "Duplicate Key error"; 19 | error = err.meta; 20 | } 21 | } 22 | 23 | res.status(statusCode).json({ 24 | success, 25 | message, 26 | error 27 | }) 28 | }; 29 | 30 | export default globalErrorHandler; -------------------------------------------------------------------------------- /src/app/middlewares/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { AnyZodObject } from "zod"; 3 | 4 | const validateRequest = (schema: AnyZodObject) => async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | await schema.parseAsync({ 7 | body: req.body 8 | }) 9 | return next(); 10 | } 11 | catch (err) { 12 | next(err) 13 | } 14 | }; 15 | 16 | export default validateRequest; -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.constant.ts: -------------------------------------------------------------------------------- 1 | export const adminFilterableFields = ['name', 'email', 'searchTerm', 'contactNumber']; 2 | 3 | export const adminSearchAbleFields = ['name', 'email', 'contactNumber']; -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from 'express'; 2 | import { AdminService } from './admin.service'; 3 | import pick from '../../../shared/pick'; 4 | import { adminFilterableFields } from './admin.constant'; 5 | import sendResponse from '../../../shared/sendResponse'; 6 | import httpStatus from 'http-status'; 7 | import catchAsync from '../../../shared/catchAsync'; 8 | 9 | 10 | const getAllFromDB: RequestHandler = catchAsync(async (req: Request, res: Response) => { 11 | // console.log(req.query) 12 | const filters = pick(req.query, adminFilterableFields); 13 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']) 14 | console.log(options) 15 | const result = await AdminService.getAllFromDB(filters, options) 16 | 17 | sendResponse(res, { 18 | statusCode: httpStatus.OK, 19 | success: true, 20 | message: "Admin data fetched!", 21 | meta: result.meta, 22 | data: result.data 23 | }) 24 | }) 25 | 26 | const getByIdFromDB = catchAsync(async (req: Request, res: Response) => { 27 | const { id } = req.params; 28 | 29 | const result = await AdminService.getByIdFromDB(id); 30 | sendResponse(res, { 31 | statusCode: httpStatus.OK, 32 | success: true, 33 | message: "Admin data fetched by id!", 34 | data: result 35 | }); 36 | }) 37 | 38 | 39 | const updateIntoDB = catchAsync(async (req: Request, res: Response) => { 40 | const { id } = req.params; 41 | 42 | const result = await AdminService.updateIntoDB(id, req.body); 43 | sendResponse(res, { 44 | statusCode: httpStatus.OK, 45 | success: true, 46 | message: "Admin data updated!", 47 | data: result 48 | }) 49 | }) 50 | 51 | const deleteFromDB = catchAsync(async (req: Request, res: Response) => { 52 | const { id } = req.params; 53 | 54 | const result = await AdminService.deleteFromDB(id); 55 | sendResponse(res, { 56 | statusCode: httpStatus.OK, 57 | success: true, 58 | message: "Admin data deleted!", 59 | data: result 60 | }) 61 | }) 62 | 63 | 64 | const softDeleteFromDB = catchAsync(async (req: Request, res: Response) => { 65 | const { id } = req.params; 66 | 67 | const result = await AdminService.softDeleteFromDB(id); 68 | sendResponse(res, { 69 | statusCode: httpStatus.OK, 70 | success: true, 71 | message: "Admin data deleted!", 72 | data: result 73 | }) 74 | }); 75 | 76 | export const AdminController = { 77 | getAllFromDB, 78 | getByIdFromDB, 79 | updateIntoDB, 80 | deleteFromDB, 81 | softDeleteFromDB 82 | } -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.interface.ts: -------------------------------------------------------------------------------- 1 | export type IAdminFilterRequest = { 2 | name?: string | undefined; 3 | email?: string | undefined; 4 | contactNumber?: string | undefined; 5 | searchTerm?: string | undefined; 6 | } -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import { AdminController } from './admin.controller'; 3 | import validateRequest from '../../middlewares/validateRequest'; 4 | import { adminValidationSchemas } from './admin.validations'; 5 | import auth from '../../middlewares/auth'; 6 | import { UserRole } from '@prisma/client'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/', 12 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 13 | AdminController.getAllFromDB 14 | ); 15 | 16 | router.get( 17 | '/:id', 18 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 19 | AdminController.getByIdFromDB 20 | ); 21 | 22 | router.patch( 23 | '/:id', 24 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 25 | validateRequest(adminValidationSchemas.update), 26 | AdminController.updateIntoDB 27 | ); 28 | 29 | router.delete( 30 | '/:id', 31 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 32 | AdminController.deleteFromDB 33 | ); 34 | 35 | router.delete( 36 | '/soft/:id', 37 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 38 | AdminController.softDeleteFromDB 39 | ); 40 | 41 | export const AdminRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { Admin, Prisma, UserStatus } from "@prisma/client"; 2 | import { adminSearchAbleFields } from "./admin.constant"; 3 | import { paginationHelper } from "../../../helpars/paginationHelper"; 4 | import prisma from "../../../shared/prisma"; 5 | import { IAdminFilterRequest } from "./admin.interface"; 6 | import { IPaginationOptions } from "../../interfaces/pagination"; 7 | 8 | const getAllFromDB = async (params: IAdminFilterRequest, options: IPaginationOptions) => { 9 | const { page, limit, skip } = paginationHelper.calculatePagination(options); 10 | const { searchTerm, ...filterData } = params; 11 | 12 | const andCondions: Prisma.AdminWhereInput[] = []; 13 | 14 | //console.log(filterData); 15 | if (params.searchTerm) { 16 | andCondions.push({ 17 | OR: adminSearchAbleFields.map(field => ({ 18 | [field]: { 19 | contains: params.searchTerm, 20 | mode: 'insensitive' 21 | } 22 | })) 23 | }) 24 | }; 25 | 26 | if (Object.keys(filterData).length > 0) { 27 | andCondions.push({ 28 | AND: Object.keys(filterData).map(key => ({ 29 | [key]: { 30 | equals: (filterData as any)[key] 31 | } 32 | })) 33 | }) 34 | }; 35 | 36 | andCondions.push({ 37 | isDeleted: false 38 | }) 39 | 40 | //console.dir(andCondions, { depth: 'inifinity' }) 41 | const whereConditons: Prisma.AdminWhereInput = { AND: andCondions } 42 | 43 | const result = await prisma.admin.findMany({ 44 | where: whereConditons, 45 | skip, 46 | take: limit, 47 | orderBy: options.sortBy && options.sortOrder ? { 48 | [options.sortBy]: options.sortOrder 49 | } : { 50 | createdAt: 'desc' 51 | } 52 | }); 53 | 54 | const total = await prisma.admin.count({ 55 | where: whereConditons 56 | }); 57 | 58 | return { 59 | meta: { 60 | page, 61 | limit, 62 | total 63 | }, 64 | data: result 65 | }; 66 | }; 67 | 68 | const getByIdFromDB = async (id: string): Promise => { 69 | const result = await prisma.admin.findUnique({ 70 | where: { 71 | id, 72 | isDeleted: false 73 | } 74 | }) 75 | 76 | return result; 77 | }; 78 | 79 | const updateIntoDB = async (id: string, data: Partial): Promise => { 80 | await prisma.admin.findUniqueOrThrow({ 81 | where: { 82 | id, 83 | isDeleted: false 84 | } 85 | }); 86 | 87 | const result = await prisma.admin.update({ 88 | where: { 89 | id 90 | }, 91 | data 92 | }); 93 | 94 | return result; 95 | }; 96 | 97 | const deleteFromDB = async (id: string): Promise => { 98 | 99 | await prisma.admin.findUniqueOrThrow({ 100 | where: { 101 | id 102 | } 103 | }); 104 | 105 | const result = await prisma.$transaction(async (transactionClient) => { 106 | const adminDeletedData = await transactionClient.admin.delete({ 107 | where: { 108 | id 109 | } 110 | }); 111 | 112 | await transactionClient.user.delete({ 113 | where: { 114 | email: adminDeletedData.email 115 | } 116 | }); 117 | 118 | return adminDeletedData; 119 | }); 120 | 121 | return result; 122 | } 123 | 124 | 125 | const softDeleteFromDB = async (id: string): Promise => { 126 | await prisma.admin.findUniqueOrThrow({ 127 | where: { 128 | id, 129 | isDeleted: false 130 | } 131 | }); 132 | 133 | const result = await prisma.$transaction(async (transactionClient) => { 134 | const adminDeletedData = await transactionClient.admin.update({ 135 | where: { 136 | id 137 | }, 138 | data: { 139 | isDeleted: true 140 | } 141 | }); 142 | 143 | await transactionClient.user.update({ 144 | where: { 145 | email: adminDeletedData.email 146 | }, 147 | data: { 148 | status: UserStatus.DELETED 149 | } 150 | }); 151 | 152 | return adminDeletedData; 153 | }); 154 | 155 | return result; 156 | } 157 | 158 | 159 | export const AdminService = { 160 | getAllFromDB, 161 | getByIdFromDB, 162 | updateIntoDB, 163 | deleteFromDB, 164 | softDeleteFromDB 165 | } -------------------------------------------------------------------------------- /src/app/modules/Admin/admin.validations.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const update = z.object({ 4 | body: z.object({ 5 | name: z.string().optional(), 6 | contactNumber: z.string().optional() 7 | }) 8 | }); 9 | 10 | 11 | export const adminValidationSchemas = { 12 | update 13 | } -------------------------------------------------------------------------------- /src/app/modules/Appointment/appointment.constant.ts: -------------------------------------------------------------------------------- 1 | export const appointmentFilterableFields: string[] = [ 2 | 'status', 3 | 'paymentStatus', 4 | 'patientEmail', 5 | 'doctorEmail' 6 | ]; -------------------------------------------------------------------------------- /src/app/modules/Appointment/appointment.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import sendResponse from "../../../shared/sendResponse"; 4 | import httpStatus from "http-status"; 5 | import { AppointmentService } from "./appointment.service"; 6 | import { IAuthUser } from "../../interfaces/common"; 7 | import pick from "../../../shared/pick"; 8 | import { appointmentFilterableFields } from "./appointment.constant"; 9 | 10 | const createAppointment = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 11 | 12 | const user = req.user; 13 | 14 | const result = await AppointmentService.createAppointment(user as IAuthUser, req.body); 15 | 16 | sendResponse(res, { 17 | statusCode: httpStatus.OK, 18 | success: true, 19 | message: "Appointment booked successfully!", 20 | data: result 21 | }) 22 | }); 23 | 24 | const getMyAppointment = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 25 | const user = req.user; 26 | const filters = pick(req.query, ['status', 'paymentStatus']); 27 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 28 | 29 | const result = await AppointmentService.getMyAppointment(user as IAuthUser, filters, options); 30 | 31 | sendResponse(res, { 32 | statusCode: httpStatus.OK, 33 | success: true, 34 | message: 'My Appointment retrive successfully', 35 | data: result 36 | }); 37 | }); 38 | 39 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 40 | const filters = pick(req.query, appointmentFilterableFields) 41 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 42 | const result = await AppointmentService.getAllFromDB(filters, options); 43 | sendResponse(res, { 44 | statusCode: httpStatus.OK, 45 | success: true, 46 | message: 'Appointment retrieval successfully', 47 | meta: result.meta, 48 | data: result.data, 49 | }); 50 | }); 51 | 52 | const changeAppointmentStatus = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 53 | const { id } = req.params; 54 | const { status } = req.body; 55 | const user = req.user; 56 | 57 | const result = await AppointmentService.changeAppointmentStatus(id, status, user as IAuthUser); 58 | sendResponse(res, { 59 | statusCode: httpStatus.OK, 60 | success: true, 61 | message: 'Appointment status changed successfully', 62 | data: result 63 | }); 64 | }); 65 | 66 | export const AppointmentController = { 67 | createAppointment, 68 | getMyAppointment, 69 | getAllFromDB, 70 | changeAppointmentStatus 71 | } -------------------------------------------------------------------------------- /src/app/modules/Appointment/appointment.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { AppointmentController } from './appointment.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import validateRequest from '../../middlewares/validateRequest'; 6 | import { AppointmentValidation } from './appointment.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * ENDPOINT: /appointment/ 12 | * 13 | * Get all appointment with filtering 14 | * Only accessable for Admin & Super Admin 15 | */ 16 | router.get( 17 | '/', 18 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 19 | AppointmentController.getAllFromDB 20 | ); 21 | 22 | router.get( 23 | '/my-appointment', 24 | auth(UserRole.PATIENT, UserRole.DOCTOR), 25 | AppointmentController.getMyAppointment 26 | ) 27 | 28 | router.post( 29 | '/', 30 | auth(UserRole.PATIENT), 31 | validateRequest(AppointmentValidation.createAppointment), 32 | AppointmentController.createAppointment 33 | ); 34 | 35 | router.patch( 36 | '/status/:id', 37 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR), 38 | AppointmentController.changeAppointmentStatus 39 | ); 40 | 41 | 42 | 43 | export const AppointmentRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Appointment/appointment.service.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../shared/prisma" 2 | import { IAuthUser } from "../../interfaces/common" 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { IPaginationOptions } from "../../interfaces/pagination"; 5 | import { paginationHelper } from "../../../helpars/paginationHelper"; 6 | import { AppointmentStatus, PaymentStatus, Prisma, UserRole } from "@prisma/client"; 7 | import ApiError from "../../errors/ApiError"; 8 | import httpStatus from "http-status"; 9 | 10 | 11 | const createAppointment = async (user: IAuthUser, payload: any) => { 12 | const patientData = await prisma.patient.findUniqueOrThrow({ 13 | where: { 14 | email: user?.email 15 | } 16 | }); 17 | 18 | const doctorData = await prisma.doctor.findUniqueOrThrow({ 19 | where: { 20 | id: payload.doctorId 21 | } 22 | }); 23 | 24 | await prisma.doctorSchedules.findFirstOrThrow({ 25 | where: { 26 | doctorId: doctorData.id, 27 | scheduleId: payload.scheduleId, 28 | isBooked: false 29 | } 30 | }); 31 | 32 | const videoCallingId: string = uuidv4(); 33 | 34 | const result = await prisma.$transaction(async (tx) => { 35 | const appointmentData = await tx.appointment.create({ 36 | data: { 37 | patientId: patientData.id, 38 | doctorId: doctorData.id, 39 | scheduleId: payload.scheduleId, 40 | videoCallingId 41 | }, 42 | include: { 43 | patient: true, 44 | doctor: true, 45 | schedule: true 46 | } 47 | }); 48 | 49 | await tx.doctorSchedules.update({ 50 | where: { 51 | doctorId_scheduleId: { 52 | doctorId: doctorData.id, 53 | scheduleId: payload.scheduleId 54 | } 55 | }, 56 | data: { 57 | isBooked: true, 58 | appointmentId: appointmentData.id 59 | } 60 | }); 61 | 62 | // PH-HealthCare-datatime 63 | const today = new Date(); 64 | 65 | const transactionId = "PH-HealthCare-" + today.getFullYear() + "-" + today.getMonth() + "-" + today.getDay() + "-" + today.getHours() + "-" + today.getMinutes(); 66 | 67 | await tx.payment.create({ 68 | data: { 69 | appointmentId: appointmentData.id, 70 | amount: doctorData.appointmentFee, 71 | transactionId 72 | } 73 | }) 74 | 75 | return appointmentData; 76 | }) 77 | 78 | return result; 79 | }; 80 | 81 | const getMyAppointment = async (user: IAuthUser, filters: any, options: IPaginationOptions) => { 82 | 83 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 84 | const { ...filterData } = filters; 85 | 86 | const andConditions: Prisma.AppointmentWhereInput[] = []; 87 | 88 | if (user?.role === UserRole.PATIENT) { 89 | andConditions.push({ 90 | patient: { 91 | email: user?.email 92 | } 93 | }) 94 | } 95 | else if (user?.role === UserRole.DOCTOR) { 96 | andConditions.push({ 97 | doctor: { 98 | email: user?.email 99 | } 100 | }) 101 | } 102 | 103 | if (Object.keys(filterData).length > 0) { 104 | const filterConditions = Object.keys(filterData).map(key => ({ 105 | [key]: { 106 | equals: (filterData as any)[key], 107 | }, 108 | })); 109 | andConditions.push(...filterConditions); 110 | } 111 | 112 | const whereConditions: Prisma.AppointmentWhereInput = 113 | andConditions.length > 0 ? { AND: andConditions } : {}; 114 | 115 | const result = await prisma.appointment.findMany({ 116 | where: whereConditions, 117 | skip, 118 | take: limit, 119 | orderBy: options.sortBy && options.sortOrder 120 | ? { [options.sortBy]: options.sortOrder } 121 | : { createdAt: 'desc' }, 122 | include: user?.role === UserRole.PATIENT 123 | ? { doctor: true, schedule: true } : { patient: { include: { medicalReport: true, patientHealthData: true } }, schedule: true } 124 | }); 125 | 126 | const total = await prisma.appointment.count({ 127 | where: whereConditions, 128 | }); 129 | 130 | return { 131 | meta: { 132 | total, 133 | page, 134 | limit, 135 | }, 136 | data: result, 137 | }; 138 | }; 139 | 140 | 141 | const getAllFromDB = async ( 142 | filters: any, 143 | options: IPaginationOptions 144 | ) => { 145 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 146 | const { patientEmail, doctorEmail, ...filterData } = filters; 147 | const andConditions = []; 148 | 149 | if (patientEmail) { 150 | andConditions.push({ 151 | patient: { 152 | email: patientEmail 153 | } 154 | }) 155 | } 156 | else if (doctorEmail) { 157 | andConditions.push({ 158 | doctor: { 159 | email: doctorEmail 160 | } 161 | }) 162 | } 163 | 164 | if (Object.keys(filterData).length > 0) { 165 | andConditions.push({ 166 | AND: Object.keys(filterData).map((key) => { 167 | return { 168 | [key]: { 169 | equals: (filterData as any)[key] 170 | } 171 | }; 172 | }) 173 | }); 174 | } 175 | 176 | // console.dir(andConditions, { depth: Infinity }) 177 | const whereConditions: Prisma.AppointmentWhereInput = 178 | andConditions.length > 0 ? { AND: andConditions } : {}; 179 | 180 | const result = await prisma.appointment.findMany({ 181 | where: whereConditions, 182 | skip, 183 | take: limit, 184 | orderBy: 185 | options.sortBy && options.sortOrder 186 | ? { [options.sortBy]: options.sortOrder } 187 | : { 188 | createdAt: 'desc', 189 | }, 190 | include: { 191 | doctor: true, 192 | patient: true 193 | } 194 | }); 195 | const total = await prisma.appointment.count({ 196 | where: whereConditions 197 | }); 198 | 199 | return { 200 | meta: { 201 | total, 202 | page, 203 | limit, 204 | }, 205 | data: result, 206 | }; 207 | }; 208 | 209 | const changeAppointmentStatus = async (appointmentId: string, status: AppointmentStatus, user: IAuthUser) => { 210 | const appointmentData = await prisma.appointment.findUniqueOrThrow({ 211 | where: { 212 | id: appointmentId 213 | }, 214 | include: { 215 | doctor: true 216 | } 217 | }); 218 | 219 | if (user?.role === UserRole.DOCTOR) { 220 | if (!(user.email === appointmentData.doctor.email)) { 221 | throw new ApiError(httpStatus.BAD_REQUEST, "This is not your appointment!") 222 | } 223 | } 224 | 225 | const result = await prisma.appointment.update({ 226 | where: { 227 | id: appointmentId 228 | }, 229 | data: { 230 | status 231 | } 232 | }); 233 | 234 | return result; 235 | 236 | } 237 | 238 | const cancelUnpaidAppointments = async () => { 239 | const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000) 240 | 241 | const unPaidAppointments = await prisma.appointment.findMany({ 242 | where: { 243 | createdAt: { 244 | lte: thirtyMinAgo 245 | }, 246 | paymentStatus: PaymentStatus.UNPAID 247 | }, 248 | }); 249 | 250 | const appointmentIdsToCancel = unPaidAppointments.map(appointment => appointment.id); 251 | 252 | await prisma.$transaction(async (tx) => { 253 | await tx.payment.deleteMany({ 254 | where: { 255 | appointmentId: { 256 | in: appointmentIdsToCancel 257 | } 258 | } 259 | }); 260 | 261 | await tx.appointment.deleteMany({ 262 | where: { 263 | id: { 264 | in: appointmentIdsToCancel 265 | } 266 | } 267 | }); 268 | 269 | for (const upPaidAppointment of unPaidAppointments) { 270 | await tx.doctorSchedules.updateMany({ 271 | where: { 272 | doctorId: upPaidAppointment.doctorId, 273 | scheduleId: upPaidAppointment.scheduleId 274 | }, 275 | data: { 276 | isBooked: false 277 | } 278 | }) 279 | } 280 | }) 281 | 282 | //console.log("updated") 283 | } 284 | 285 | export const AppointmentService = { 286 | createAppointment, 287 | getMyAppointment, 288 | getAllFromDB, 289 | changeAppointmentStatus, 290 | cancelUnpaidAppointments 291 | } -------------------------------------------------------------------------------- /src/app/modules/Appointment/appointment.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const createAppointment = z.object({ 4 | body: z.object({ 5 | doctorId: z.string({ 6 | required_error: "Doctor Id is required!" 7 | }), 8 | scheduleId: z.string({ 9 | required_error: "Doctor schedule id is required!" 10 | }) 11 | }) 12 | }); 13 | 14 | export const AppointmentValidation = { 15 | createAppointment 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import { AuthServices } from "./auth.service"; 4 | import sendResponse from "../../../shared/sendResponse"; 5 | import httpStatus from "http-status"; 6 | 7 | const loginUser = catchAsync(async (req: Request, res: Response) => { 8 | const result = await AuthServices.loginUser(req.body); 9 | 10 | const { refreshToken } = result; 11 | 12 | res.cookie('refreshToken', refreshToken, { 13 | secure: false, 14 | httpOnly: true 15 | }); 16 | 17 | sendResponse(res, { 18 | statusCode: httpStatus.OK, 19 | success: true, 20 | message: "Logged in successfully!", 21 | data: { 22 | accessToken: result.accessToken, 23 | needPasswordChange: result.needPasswordChange 24 | } 25 | }) 26 | }); 27 | 28 | const refreshToken = catchAsync(async (req: Request, res: Response) => { 29 | const { refreshToken } = req.cookies; 30 | 31 | const result = await AuthServices.refreshToken(refreshToken); 32 | 33 | sendResponse(res, { 34 | statusCode: httpStatus.OK, 35 | success: true, 36 | message: "Access token genereated successfully!", 37 | data: result 38 | // data: { 39 | // accessToken: result.accessToken, 40 | // needPasswordChange: result.needPasswordChange 41 | // } 42 | }) 43 | }); 44 | 45 | const changePassword = catchAsync(async (req: Request & { user?: any }, res: Response) => { 46 | const user = req.user; 47 | 48 | const result = await AuthServices.changePassword(user, req.body); 49 | 50 | sendResponse(res, { 51 | statusCode: httpStatus.OK, 52 | success: true, 53 | message: "Password Changed successfully", 54 | data: result 55 | }) 56 | }); 57 | 58 | const forgotPassword = catchAsync(async (req: Request, res: Response) => { 59 | 60 | await AuthServices.forgotPassword(req.body); 61 | 62 | sendResponse(res, { 63 | statusCode: httpStatus.OK, 64 | success: true, 65 | message: "Check your email!", 66 | data: null 67 | }) 68 | }); 69 | 70 | const resetPassword = catchAsync(async (req: Request, res: Response) => { 71 | 72 | const token = req.headers.authorization || ""; 73 | 74 | await AuthServices.resetPassword(token, req.body); 75 | 76 | sendResponse(res, { 77 | statusCode: httpStatus.OK, 78 | success: true, 79 | message: "Password Reset!", 80 | data: null 81 | }) 82 | }); 83 | 84 | 85 | export const AuthController = { 86 | loginUser, 87 | refreshToken, 88 | changePassword, 89 | forgotPassword, 90 | resetPassword 91 | }; -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { AuthController } from './auth.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | 6 | const router = express.Router(); 7 | 8 | router.post( 9 | '/login', 10 | AuthController.loginUser 11 | ); 12 | 13 | router.post( 14 | '/refresh-token', 15 | AuthController.refreshToken 16 | ) 17 | 18 | router.post( 19 | '/change-password', 20 | auth( 21 | UserRole.SUPER_ADMIN, 22 | UserRole.ADMIN, 23 | UserRole.DOCTOR, 24 | UserRole.PATIENT 25 | ), 26 | AuthController.changePassword 27 | ); 28 | 29 | router.post( 30 | '/forgot-password', 31 | AuthController.forgotPassword 32 | ); 33 | 34 | router.post( 35 | '/reset-password', 36 | AuthController.resetPassword 37 | ) 38 | 39 | export const AuthRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { UserStatus } from "@prisma/client"; 2 | import { jwtHelpers } from "../../../helpars/jwtHelpers"; 3 | import prisma from "../../../shared/prisma"; 4 | import * as bcrypt from 'bcrypt' 5 | import config from "../../../config"; 6 | import { Secret } from "jsonwebtoken"; 7 | import emailSender from "./emailSender"; 8 | import ApiError from "../../errors/ApiError"; 9 | import httpStatus from "http-status"; 10 | 11 | const loginUser = async (payload: { 12 | email: string, 13 | password: string 14 | }) => { 15 | const userData = await prisma.user.findUniqueOrThrow({ 16 | where: { 17 | email: payload.email, 18 | status: UserStatus.ACTIVE 19 | } 20 | }); 21 | 22 | const isCorrectPassword: boolean = await bcrypt.compare(payload.password, userData.password); 23 | 24 | if (!isCorrectPassword) { 25 | throw new Error("Password incorrect!") 26 | } 27 | const accessToken = jwtHelpers.generateToken({ 28 | email: userData.email, 29 | role: userData.role 30 | }, 31 | config.jwt.jwt_secret as Secret, 32 | config.jwt.expires_in as string 33 | ); 34 | 35 | const refreshToken = jwtHelpers.generateToken({ 36 | email: userData.email, 37 | role: userData.role 38 | }, 39 | config.jwt.refresh_token_secret as Secret, 40 | config.jwt.refresh_token_expires_in as string 41 | ); 42 | 43 | return { 44 | accessToken, 45 | refreshToken, 46 | needPasswordChange: userData.needPasswordChange 47 | }; 48 | }; 49 | 50 | const refreshToken = async (token: string) => { 51 | let decodedData; 52 | try { 53 | decodedData = jwtHelpers.verifyToken(token, config.jwt.refresh_token_secret as Secret); 54 | } 55 | catch (err) { 56 | throw new Error("You are not authorized!") 57 | } 58 | 59 | const userData = await prisma.user.findUniqueOrThrow({ 60 | where: { 61 | email: decodedData.email, 62 | status: UserStatus.ACTIVE 63 | } 64 | }); 65 | 66 | const accessToken = jwtHelpers.generateToken({ 67 | email: userData.email, 68 | role: userData.role 69 | }, 70 | config.jwt.jwt_secret as Secret, 71 | config.jwt.expires_in as string 72 | ); 73 | 74 | return { 75 | accessToken, 76 | needPasswordChange: userData.needPasswordChange 77 | }; 78 | 79 | }; 80 | 81 | const changePassword = async (user: any, payload: any) => { 82 | const userData = await prisma.user.findUniqueOrThrow({ 83 | where: { 84 | email: user.email, 85 | status: UserStatus.ACTIVE 86 | } 87 | }); 88 | 89 | const isCorrectPassword: boolean = await bcrypt.compare(payload.oldPassword, userData.password); 90 | 91 | if (!isCorrectPassword) { 92 | throw new Error("Password incorrect!") 93 | } 94 | 95 | const hashedPassword: string = await bcrypt.hash(payload.newPassword, 12); 96 | 97 | await prisma.user.update({ 98 | where: { 99 | email: userData.email 100 | }, 101 | data: { 102 | password: hashedPassword, 103 | needPasswordChange: false 104 | } 105 | }) 106 | 107 | return { 108 | message: "Password changed successfully!" 109 | } 110 | }; 111 | 112 | const forgotPassword = async (payload: { email: string }) => { 113 | const userData = await prisma.user.findUniqueOrThrow({ 114 | where: { 115 | email: payload.email, 116 | status: UserStatus.ACTIVE 117 | } 118 | }); 119 | 120 | const resetPassToken = jwtHelpers.generateToken( 121 | { email: userData.email, role: userData.role }, 122 | config.jwt.reset_pass_secret as Secret, 123 | config.jwt.reset_pass_token_expires_in as string 124 | ) 125 | //console.log(resetPassToken) 126 | 127 | const resetPassLink = config.reset_pass_link + `?userId=${userData.id}&token=${resetPassToken}` 128 | 129 | await emailSender( 130 | userData.email, 131 | ` 132 |
133 |

Dear User,

134 |

Your password reset link 135 | 136 | 139 | 140 |

141 | 142 |
143 | ` 144 | ) 145 | //console.log(resetPassLink) 146 | }; 147 | 148 | const resetPassword = async (token: string, payload: { id: string, password: string }) => { 149 | console.log({ token, payload }) 150 | 151 | const userData = await prisma.user.findUniqueOrThrow({ 152 | where: { 153 | id: payload.id, 154 | status: UserStatus.ACTIVE 155 | } 156 | }); 157 | 158 | const isValidToken = jwtHelpers.verifyToken(token, config.jwt.reset_pass_secret as Secret) 159 | 160 | if (!isValidToken) { 161 | throw new ApiError(httpStatus.FORBIDDEN, "Forbidden!") 162 | } 163 | 164 | // hash password 165 | const password = await bcrypt.hash(payload.password, 12); 166 | 167 | // update into database 168 | await prisma.user.update({ 169 | where: { 170 | id: payload.id 171 | }, 172 | data: { 173 | password 174 | } 175 | }) 176 | }; 177 | 178 | export const AuthServices = { 179 | loginUser, 180 | refreshToken, 181 | changePassword, 182 | forgotPassword, 183 | resetPassword 184 | } -------------------------------------------------------------------------------- /src/app/modules/Auth/emailSender.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | import config from '../../../config'; 3 | 4 | const emailSender = async ( 5 | email: string, 6 | html: string 7 | ) => { 8 | const transporter = nodemailer.createTransport({ 9 | host: "smtp.gmail.com", 10 | port: 587, 11 | secure: false, // Use `true` for port 465, `false` for all other ports 12 | auth: { 13 | user: config.emailSender.email, 14 | pass: config.emailSender.app_pass, 15 | }, 16 | tls: { 17 | rejectUnauthorized: false 18 | } 19 | }); 20 | 21 | const info = await transporter.sendMail({ 22 | from: '"PH Health Care" ', // sender address 23 | to: email, // list of receivers 24 | subject: "Reset Password Link", // Subject line 25 | //text: "Hello world?", // plain text body 26 | html, // html body 27 | }); 28 | 29 | //console.log("Message sent: %s", info.messageId); 30 | } 31 | 32 | export default emailSender; -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.constants.ts: -------------------------------------------------------------------------------- 1 | export const doctorSearchableFields: string[] = [ 2 | 'name', 3 | 'email', 4 | 'contactNumber', 5 | 'address', 6 | 'qualification', 7 | 'designation' 8 | ]; 9 | 10 | export const doctorFilterableFields: string[] = [ 11 | 'searchTerm', 12 | 'email', 13 | 'contactNumber', 14 | 'gender', 15 | 'apointmentFee', 16 | 'specialties' 17 | ]; 18 | -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from 'express'; 2 | import sendResponse from '../../../shared/sendResponse'; 3 | import httpStatus from 'http-status'; 4 | import catchAsync from '../../../shared/catchAsync'; 5 | import { DoctorService } from './doctor.service'; 6 | import pick from '../../../shared/pick'; 7 | import { doctorFilterableFields } from './doctor.constants'; 8 | 9 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 10 | const filters = pick(req.query, doctorFilterableFields); 11 | 12 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 13 | 14 | const result = await DoctorService.getAllFromDB(filters, options); 15 | 16 | sendResponse(res, { 17 | statusCode: httpStatus.OK, 18 | success: true, 19 | message: 'Doctors retrieval successfully', 20 | meta: result.meta, 21 | data: result.data, 22 | }); 23 | }); 24 | 25 | const getByIdFromDB = catchAsync(async (req: Request, res: Response) => { 26 | const { id } = req.params; 27 | const result = await DoctorService.getByIdFromDB(id); 28 | sendResponse(res, { 29 | statusCode: httpStatus.OK, 30 | success: true, 31 | message: 'Doctor retrieval successfully', 32 | data: result, 33 | }); 34 | }); 35 | 36 | const updateIntoDB = catchAsync(async (req: Request, res: Response) => { 37 | 38 | const { id } = req.params; 39 | const result = await DoctorService.updateIntoDB(id, req.body); 40 | 41 | sendResponse(res, { 42 | statusCode: httpStatus.OK, 43 | success: true, 44 | message: "Doctor data updated!", 45 | data: result 46 | }) 47 | }); 48 | 49 | const deleteFromDB = catchAsync(async (req: Request, res: Response) => { 50 | const { id } = req.params; 51 | const result = await DoctorService.deleteFromDB(id); 52 | sendResponse(res, { 53 | statusCode: httpStatus.OK, 54 | success: true, 55 | message: 'Doctor deleted successfully', 56 | data: result, 57 | }); 58 | }); 59 | 60 | 61 | const softDelete = catchAsync(async (req: Request, res: Response) => { 62 | const { id } = req.params; 63 | const result = await DoctorService.softDelete(id); 64 | sendResponse(res, { 65 | statusCode: httpStatus.OK, 66 | success: true, 67 | message: 'Doctor soft deleted successfully', 68 | data: result, 69 | }); 70 | }); 71 | 72 | 73 | export const DoctorController = { 74 | updateIntoDB, 75 | getAllFromDB, 76 | getByIdFromDB, 77 | deleteFromDB, 78 | softDelete 79 | } -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.interface.ts: -------------------------------------------------------------------------------- 1 | export type IDoctorFilterRequest = { 2 | searchTerm?: string | undefined; 3 | email?: string | undefined; 4 | contactNo?: string | undefined; 5 | gender?: string | undefined; 6 | specialties?: string | undefined; 7 | }; 8 | 9 | export type IDoctorUpdate = { 10 | name: string; 11 | profilePhoto: string; 12 | contactNumber: string; 13 | address: string; 14 | registrationNumber: string; 15 | experience: number; 16 | gender: 'MALE' | 'FEMALE'; 17 | apointmentFee: number; 18 | qualification: string; 19 | currentWorkingPlace: string; 20 | designation: string; 21 | specialties: ISpecialties[]; 22 | }; 23 | 24 | export type ISpecialties = { 25 | specialtiesId: string; 26 | isDeleted?: null; 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { DoctorController } from './doctor.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import validateRequest from '../../middlewares/validateRequest'; 6 | import { DoctorValidation } from './doctor.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | // task 3 11 | router.get('/', DoctorController.getAllFromDB); 12 | 13 | //task 4 14 | router.get('/:id', DoctorController.getByIdFromDB); 15 | 16 | router.patch( 17 | '/:id', 18 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR), 19 | validateRequest(DoctorValidation.update), 20 | DoctorController.updateIntoDB 21 | ); 22 | 23 | //task 5 24 | router.delete( 25 | '/:id', 26 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 27 | DoctorController.deleteFromDB 28 | ); 29 | 30 | // task 6 31 | router.delete( 32 | '/soft/:id', 33 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 34 | DoctorController.softDelete); 35 | 36 | export const DoctorRoutes = router -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.service.ts: -------------------------------------------------------------------------------- 1 | import { Doctor, Prisma, UserStatus } from "@prisma/client"; 2 | import prisma from "../../../shared/prisma"; 3 | import { IDoctorFilterRequest, IDoctorUpdate } from "./doctor.interface"; 4 | import { IPaginationOptions } from "../../interfaces/pagination"; 5 | import { paginationHelper } from "../../../helpars/paginationHelper"; 6 | import { doctorSearchableFields } from "./doctor.constants"; 7 | 8 | const getAllFromDB = async ( 9 | filters: IDoctorFilterRequest, 10 | options: IPaginationOptions, 11 | ) => { 12 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 13 | const { searchTerm, specialties, ...filterData } = filters; 14 | 15 | const andConditions: Prisma.DoctorWhereInput[] = []; 16 | 17 | if (searchTerm) { 18 | andConditions.push({ 19 | OR: doctorSearchableFields.map(field => ({ 20 | [field]: { 21 | contains: searchTerm, 22 | mode: 'insensitive', 23 | }, 24 | })), 25 | }); 26 | }; 27 | 28 | // doctor > doctorSpecialties > specialties -> title 29 | 30 | if (specialties && specialties.length > 0) { 31 | andConditions.push({ 32 | doctorSpecialties: { 33 | some: { 34 | specialities: { 35 | title: { 36 | contains: specialties, 37 | mode: 'insensitive' 38 | } 39 | } 40 | } 41 | } 42 | }) 43 | }; 44 | 45 | 46 | if (Object.keys(filterData).length > 0) { 47 | const filterConditions = Object.keys(filterData).map(key => ({ 48 | [key]: { 49 | equals: (filterData as any)[key], 50 | }, 51 | })); 52 | andConditions.push(...filterConditions); 53 | } 54 | 55 | andConditions.push({ 56 | isDeleted: false, 57 | }); 58 | 59 | const whereConditions: Prisma.DoctorWhereInput = 60 | andConditions.length > 0 ? { AND: andConditions } : {}; 61 | 62 | const result = await prisma.doctor.findMany({ 63 | where: whereConditions, 64 | skip, 65 | take: limit, 66 | orderBy: options.sortBy && options.sortOrder 67 | ? { [options.sortBy]: options.sortOrder } 68 | : { averageRating: 'desc' }, 69 | include: { 70 | doctorSpecialties: { 71 | include: { 72 | specialities: true 73 | } 74 | }, 75 | review: { 76 | select: { 77 | rating: true 78 | } 79 | } 80 | }, 81 | }); 82 | 83 | const total = await prisma.doctor.count({ 84 | where: whereConditions, 85 | }); 86 | 87 | return { 88 | meta: { 89 | total, 90 | page, 91 | limit, 92 | }, 93 | data: result, 94 | }; 95 | }; 96 | 97 | const getByIdFromDB = async (id: string): Promise => { 98 | const result = await prisma.doctor.findUnique({ 99 | where: { 100 | id, 101 | isDeleted: false, 102 | }, 103 | include: { 104 | doctorSpecialties: { 105 | include: { 106 | specialities: true 107 | } 108 | }, 109 | review: true 110 | } 111 | }); 112 | return result; 113 | }; 114 | 115 | const updateIntoDB = async (id: string, payload: IDoctorUpdate) => { 116 | const { specialties, ...doctorData } = payload; 117 | 118 | const doctorInfo = await prisma.doctor.findUniqueOrThrow({ 119 | where: { 120 | id 121 | } 122 | }); 123 | 124 | await prisma.$transaction(async (transactionClient) => { 125 | await transactionClient.doctor.update({ 126 | where: { 127 | id 128 | }, 129 | data: doctorData 130 | }); 131 | 132 | if (specialties && specialties.length > 0) { 133 | // delete specialties 134 | const deleteSpecialtiesIds = specialties.filter(specialty => specialty.isDeleted); 135 | //console.log(deleteSpecialtiesIds) 136 | for (const specialty of deleteSpecialtiesIds) { 137 | await transactionClient.doctorSpecialties.deleteMany({ 138 | where: { 139 | doctorId: doctorInfo.id, 140 | specialitiesId: specialty.specialtiesId 141 | } 142 | }); 143 | } 144 | 145 | // create specialties 146 | const createSpecialtiesIds = specialties.filter(specialty => !specialty.isDeleted); 147 | console.log(createSpecialtiesIds) 148 | for (const specialty of createSpecialtiesIds) { 149 | await transactionClient.doctorSpecialties.create({ 150 | data: { 151 | doctorId: doctorInfo.id, 152 | specialitiesId: specialty.specialtiesId 153 | } 154 | }); 155 | } 156 | } 157 | }) 158 | 159 | const result = await prisma.doctor.findUnique({ 160 | where: { 161 | id: doctorInfo.id 162 | }, 163 | include: { 164 | doctorSpecialties: { 165 | include: { 166 | specialities: true 167 | } 168 | } 169 | } 170 | }) 171 | return result; 172 | }; 173 | 174 | const deleteFromDB = async (id: string): Promise => { 175 | return await prisma.$transaction(async transactionClient => { 176 | const deleteDoctor = await transactionClient.doctor.delete({ 177 | where: { 178 | id, 179 | }, 180 | }); 181 | 182 | await transactionClient.user.delete({ 183 | where: { 184 | email: deleteDoctor.email, 185 | }, 186 | }); 187 | 188 | return deleteDoctor; 189 | }); 190 | }; 191 | 192 | const softDelete = async (id: string): Promise => { 193 | return await prisma.$transaction(async transactionClient => { 194 | const deleteDoctor = await transactionClient.doctor.update({ 195 | where: { id }, 196 | data: { 197 | isDeleted: true, 198 | }, 199 | }); 200 | 201 | await transactionClient.user.update({ 202 | where: { 203 | email: deleteDoctor.email, 204 | }, 205 | data: { 206 | status: UserStatus.DELETED, 207 | }, 208 | }); 209 | 210 | return deleteDoctor; 211 | }); 212 | }; 213 | 214 | 215 | 216 | export const DoctorService = { 217 | updateIntoDB, 218 | getAllFromDB, 219 | getByIdFromDB, 220 | deleteFromDB, 221 | softDelete 222 | } -------------------------------------------------------------------------------- /src/app/modules/Doctor/doctor.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const create = z.object({ 4 | body: z.object({ 5 | email: z.string({ 6 | required_error: 'Email is required', 7 | }), 8 | name: z.string({ 9 | required_error: 'Name is required', 10 | }), 11 | profilePhoto: z.string({ 12 | required_error: 'Profile Photo is required', 13 | }), 14 | contactNumber: z.string({ 15 | required_error: 'Contact Number is required', 16 | }), 17 | registrationNumber: z.string({ 18 | required_error: 'Registration Number is required', 19 | }), 20 | experience: z.number({ 21 | required_error: 'Experience is required', 22 | }), 23 | gender: z.string({ 24 | required_error: 'Gender is required', 25 | }), 26 | apointmentFee: z.number({ 27 | required_error: 'Blood group is required', 28 | }), 29 | qualification: z.string({ 30 | required_error: 'Apointment Fee is required', 31 | }), 32 | currentWorkingPlace: z.string({ 33 | required_error: 'Current Working Place is required', 34 | }), 35 | designation: z.string({ 36 | required_error: 'Designation is required', 37 | }), 38 | }), 39 | }); 40 | 41 | const update = z.object({ 42 | body: z.object({ 43 | name: z.string().optional(), 44 | profilePhoto: z.string().optional(), 45 | contactNumber: z.string().optional(), 46 | registrationNumber: z.string().optional(), 47 | experience: z.number().optional(), 48 | gender: z.string().optional(), 49 | apointmentFee: z.number().optional(), 50 | qualification: z.string().optional(), 51 | currentWorkingPlace: z.string().optional(), 52 | designation: z.string().optional(), 53 | }), 54 | }); 55 | 56 | export const DoctorValidation = { 57 | create, 58 | update, 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.constants.ts: -------------------------------------------------------------------------------- 1 | export const scheduleFilterableFields: string[] = ['searchTerm', 'isBooked', 'doctorId']; 2 | -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import sendResponse from "../../../shared/sendResponse"; 4 | import httpStatus from "http-status"; 5 | import { DoctorScheduleService } from "./doctorSchedule.service"; 6 | import { IAuthUser } from "../../interfaces/common"; 7 | import pick from "../../../shared/pick"; 8 | import { scheduleFilterableFields } from "./doctorSchedule.constants"; 9 | 10 | const insertIntoDB = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 11 | 12 | const user = req.user; 13 | const result = await DoctorScheduleService.insertIntoDB(user, req.body); 14 | 15 | sendResponse(res, { 16 | statusCode: httpStatus.OK, 17 | success: true, 18 | message: "Doctor Schedule created successfully!", 19 | data: result 20 | }); 21 | }); 22 | 23 | const getMySchedule = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 24 | const filters = pick(req.query, ['startDate', 'endDate', 'isBooked']); 25 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 26 | 27 | const user = req.user; 28 | const result = await DoctorScheduleService.getMySchedule(filters, options, user as IAuthUser); 29 | 30 | sendResponse(res, { 31 | statusCode: httpStatus.OK, 32 | success: true, 33 | message: "My Schedule fetched successfully!", 34 | data: result 35 | }); 36 | }); 37 | 38 | const deleteFromDB = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 39 | 40 | const user = req.user; 41 | const { id } = req.params; 42 | const result = await DoctorScheduleService.deleteFromDB(user as IAuthUser, id); 43 | 44 | sendResponse(res, { 45 | statusCode: httpStatus.OK, 46 | success: true, 47 | message: "My Schedule deleted successfully!", 48 | data: result 49 | }); 50 | }); 51 | 52 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 53 | const filters = pick(req.query, scheduleFilterableFields); 54 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 55 | const result = await DoctorScheduleService.getAllFromDB(filters, options); 56 | sendResponse(res, { 57 | statusCode: httpStatus.OK, 58 | success: true, 59 | message: 'Doctor Schedule retrieval successfully', 60 | meta: result.meta, 61 | data: result.data, 62 | }); 63 | }); 64 | 65 | export const DoctorScheduleController = { 66 | insertIntoDB, 67 | getMySchedule, 68 | deleteFromDB, 69 | getAllFromDB 70 | }; -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.interface.ts: -------------------------------------------------------------------------------- 1 | export type IDoctorScheduleFilterRequest = { 2 | searchTerm?: string | undefined; 3 | isBooked?: boolean | undefined; 4 | }; -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { DoctorScheduleController } from './doctorSchedule.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import validateRequest from '../../middlewares/validateRequest'; 6 | import { DoctorScheduleValidation } from './doctorSchedule.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * API ENDPOINT: /doctor-schedule/ 12 | * 13 | * Get all doctor schedule with filtering 14 | */ 15 | router.get( 16 | '/', 17 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT), 18 | DoctorScheduleController.getAllFromDB 19 | ); 20 | 21 | router.get( 22 | '/my-schedule', 23 | auth(UserRole.DOCTOR), 24 | DoctorScheduleController.getMySchedule 25 | ) 26 | 27 | router.post( 28 | '/', 29 | auth(UserRole.DOCTOR), 30 | validateRequest(DoctorScheduleValidation.create), 31 | DoctorScheduleController.insertIntoDB 32 | ); 33 | 34 | router.delete( 35 | '/:id', 36 | auth(UserRole.DOCTOR), 37 | DoctorScheduleController.deleteFromDB 38 | ); 39 | 40 | 41 | export const DoctorScheduleRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.service.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { paginationHelper } from "../../../helpars/paginationHelper"; 3 | import prisma from "../../../shared/prisma"; 4 | import { IAuthUser } from "../../interfaces/common"; 5 | import { IPaginationOptions } from "../../interfaces/pagination"; 6 | import ApiError from "../../errors/ApiError"; 7 | import httpStatus from "http-status"; 8 | import { IDoctorScheduleFilterRequest } from "./doctorSchedule.interface"; 9 | 10 | const insertIntoDB = async (user: any, payload: { 11 | scheduleIds: string[] 12 | }) => { 13 | const doctorData = await prisma.doctor.findUniqueOrThrow({ 14 | where: { 15 | email: user.email 16 | } 17 | }); 18 | 19 | const doctorScheduleData = payload.scheduleIds.map(scheduleId => ({ 20 | doctorId: doctorData.id, 21 | scheduleId 22 | })) 23 | 24 | const result = await prisma.doctorSchedules.createMany({ 25 | data: doctorScheduleData 26 | }); 27 | 28 | return result; 29 | }; 30 | 31 | 32 | const getMySchedule = async ( 33 | filters: any, 34 | options: IPaginationOptions, 35 | user: IAuthUser 36 | ) => { 37 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 38 | const { startDate, endDate, ...filterData } = filters; 39 | //console.log(filterData) 40 | 41 | const andConditions = []; 42 | 43 | if (startDate && endDate) { 44 | andConditions.push({ 45 | AND: [ 46 | { 47 | schedule: { 48 | startDateTime: { 49 | gte: startDate 50 | } 51 | } 52 | }, 53 | { 54 | schedule: { 55 | endDateTime: { 56 | lte: endDate 57 | } 58 | } 59 | } 60 | ] 61 | }) 62 | }; 63 | 64 | 65 | if (Object.keys(filterData).length > 0) { 66 | 67 | if (typeof filterData.isBooked === 'string' && filterData.isBooked === 'true') { 68 | filterData.isBooked = true 69 | } 70 | else if (typeof filterData.isBooked === 'string' && filterData.isBooked === 'false') { 71 | filterData.isBooked = false 72 | } 73 | 74 | andConditions.push({ 75 | AND: Object.keys(filterData).map(key => { 76 | return { 77 | [key]: { 78 | equals: (filterData as any)[key], 79 | }, 80 | }; 81 | }), 82 | }); 83 | } 84 | 85 | const whereConditions: Prisma.DoctorSchedulesWhereInput = 86 | andConditions.length > 0 ? { AND: andConditions } : {}; 87 | 88 | 89 | const result = await prisma.doctorSchedules.findMany({ 90 | where: whereConditions, 91 | skip, 92 | take: limit, 93 | orderBy: 94 | options.sortBy && options.sortOrder 95 | ? { [options.sortBy]: options.sortOrder } 96 | : { 97 | 98 | } 99 | }); 100 | const total = await prisma.doctorSchedules.count({ 101 | where: whereConditions 102 | }); 103 | 104 | return { 105 | meta: { 106 | total, 107 | page, 108 | limit, 109 | }, 110 | data: result, 111 | }; 112 | }; 113 | 114 | const deleteFromDB = async (user: IAuthUser, scheduleId: string) => { 115 | 116 | const doctorData = await prisma.doctor.findUniqueOrThrow({ 117 | where: { 118 | email: user?.email 119 | } 120 | }); 121 | 122 | const isBookedSchedule = await prisma.doctorSchedules.findFirst({ 123 | where: { 124 | doctorId: doctorData.id, 125 | scheduleId: scheduleId, 126 | isBooked: true 127 | } 128 | }); 129 | 130 | if (isBookedSchedule) { 131 | throw new ApiError(httpStatus.BAD_REQUEST, "You can not delete the schedule because of the schedule is already booked!") 132 | } 133 | 134 | const result = await prisma.doctorSchedules.delete({ 135 | where: { 136 | doctorId_scheduleId: { 137 | doctorId: doctorData.id, 138 | scheduleId: scheduleId 139 | } 140 | } 141 | }) 142 | return result; 143 | 144 | } 145 | 146 | const getAllFromDB = async ( 147 | filters: IDoctorScheduleFilterRequest, 148 | options: IPaginationOptions, 149 | ) => { 150 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 151 | const { searchTerm, ...filterData } = filters; 152 | const andConditions = []; 153 | 154 | if (searchTerm) { 155 | andConditions.push({ 156 | doctor: { 157 | name: { 158 | contains: searchTerm, 159 | mode: 'insensitive', 160 | }, 161 | }, 162 | }); 163 | } 164 | 165 | if (Object.keys(filterData).length > 0) { 166 | if (typeof filterData.isBooked === 'string' && filterData.isBooked === 'true') { 167 | filterData.isBooked = true; 168 | } else if (typeof filterData.isBooked === 'string' && filterData.isBooked === 'false') { 169 | filterData.isBooked = false; 170 | } 171 | andConditions.push({ 172 | AND: Object.keys(filterData).map((key) => ({ 173 | [key]: { 174 | equals: (filterData as any)[key] 175 | } 176 | })) 177 | }); 178 | } 179 | 180 | const whereConditions: any = 181 | andConditions.length > 0 ? { AND: andConditions } : {}; 182 | const result = await prisma.doctorSchedules.findMany({ 183 | include: { 184 | doctor: true, 185 | schedule: true, 186 | }, 187 | where: whereConditions, 188 | skip, 189 | take: limit, 190 | orderBy: 191 | options.sortBy && options.sortOrder 192 | ? { [options.sortBy]: options.sortOrder } 193 | : {}, 194 | }); 195 | const total = await prisma.doctorSchedules.count({ 196 | where: whereConditions, 197 | }); 198 | 199 | return { 200 | meta: { 201 | total, 202 | page, 203 | limit, 204 | }, 205 | data: result, 206 | }; 207 | }; 208 | 209 | 210 | 211 | export const DoctorScheduleService = { 212 | insertIntoDB, 213 | getMySchedule, 214 | deleteFromDB, 215 | getAllFromDB 216 | } -------------------------------------------------------------------------------- /src/app/modules/DoctorSchedule/doctorSchedule.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const create = z.object({ 4 | body: z.object({ 5 | scheduleIds: z.array(z.string()), 6 | }), 7 | }); 8 | 9 | export const DoctorScheduleValidation = { 10 | create, 11 | }; -------------------------------------------------------------------------------- /src/app/modules/Meta/meta.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import { MetaService } from "./meta.service"; 4 | import sendResponse from "../../../shared/sendResponse"; 5 | import httpStatus from "http-status"; 6 | import { IAuthUser } from "../../interfaces/common"; 7 | 8 | const fetchDashboardMetaData = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 9 | 10 | const user = req.user; 11 | const result = await MetaService.fetchDashboardMetaData(user as IAuthUser); 12 | 13 | sendResponse(res, { 14 | statusCode: httpStatus.OK, 15 | success: true, 16 | message: "Meta data retrival successfully!", 17 | data: result 18 | }) 19 | }); 20 | 21 | export const MetaController = { 22 | fetchDashboardMetaData 23 | } -------------------------------------------------------------------------------- /src/app/modules/Meta/meta.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { MetaController } from './meta.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | 6 | const router = express.Router(); 7 | 8 | router.get( 9 | '/', 10 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT), 11 | MetaController.fetchDashboardMetaData 12 | ) 13 | 14 | 15 | export const MetaRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Meta/meta.service.ts: -------------------------------------------------------------------------------- 1 | import { PaymentStatus, UserRole } from "@prisma/client"; 2 | import { IAuthUser } from "../../interfaces/common"; 3 | import ApiError from "../../errors/ApiError"; 4 | import prisma from "../../../shared/prisma"; 5 | 6 | const fetchDashboardMetaData = async (user: IAuthUser) => { 7 | let metaData; 8 | switch (user?.role) { 9 | case UserRole.SUPER_ADMIN: 10 | metaData = getSuperAdminMetaData(); 11 | break; 12 | case UserRole.ADMIN: 13 | metaData = getAdminMetaData(); 14 | break; 15 | case UserRole.DOCTOR: 16 | metaData = getDoctorMetaData(user as IAuthUser); 17 | break; 18 | case UserRole.PATIENT: 19 | metaData = getPatientMetaData(user) 20 | break; 21 | default: 22 | throw new Error('Invalid user role!') 23 | } 24 | 25 | return metaData; 26 | }; 27 | 28 | const getSuperAdminMetaData = async () => { 29 | const appointmentCount = await prisma.appointment.count(); 30 | const patientCount = await prisma.patient.count(); 31 | const doctorCount = await prisma.doctor.count(); 32 | const adminCount = await prisma.admin.count(); 33 | const paymentCount = await prisma.payment.count(); 34 | 35 | const totalRevenue = await prisma.payment.aggregate({ 36 | _sum: { amount: true }, 37 | where: { 38 | status: PaymentStatus.PAID 39 | } 40 | }); 41 | 42 | const barChartData = await getBarChartData(); 43 | const pieCharData = await getPieChartData(); 44 | 45 | return { appointmentCount, patientCount, doctorCount, adminCount, paymentCount, totalRevenue, barChartData, pieCharData } 46 | } 47 | 48 | const getAdminMetaData = async () => { 49 | const appointmentCount = await prisma.appointment.count(); 50 | const patientCount = await prisma.patient.count(); 51 | const doctorCount = await prisma.doctor.count(); 52 | const paymentCount = await prisma.payment.count(); 53 | 54 | const totalRevenue = await prisma.payment.aggregate({ 55 | _sum: { amount: true }, 56 | where: { 57 | status: PaymentStatus.PAID 58 | } 59 | }); 60 | 61 | const barChartData = await getBarChartData(); 62 | const pieCharData = await getPieChartData(); 63 | 64 | return { appointmentCount, patientCount, doctorCount, paymentCount, totalRevenue, barChartData, pieCharData } 65 | } 66 | 67 | const getDoctorMetaData = async (user: IAuthUser) => { 68 | const doctorData = await prisma.doctor.findUniqueOrThrow({ 69 | where: { 70 | email: user?.email 71 | } 72 | }); 73 | 74 | const appointmentCount = await prisma.appointment.count({ 75 | where: { 76 | doctorId: doctorData.id 77 | } 78 | }); 79 | 80 | const patientCount = await prisma.appointment.groupBy({ 81 | by: ['patientId'], 82 | _count: { 83 | id: true 84 | } 85 | }); 86 | 87 | const reviewCount = await prisma.review.count({ 88 | where: { 89 | doctorId: doctorData.id 90 | } 91 | }); 92 | 93 | const totalRevenue = await prisma.payment.aggregate({ 94 | _sum: { 95 | amount: true 96 | }, 97 | where: { 98 | appointment: { 99 | doctorId: doctorData.id 100 | }, 101 | status: PaymentStatus.PAID 102 | } 103 | }); 104 | 105 | const appointmentStatusDistribution = await prisma.appointment.groupBy({ 106 | by: ['status'], 107 | _count: { id: true }, 108 | where: { 109 | doctorId: doctorData.id 110 | } 111 | }); 112 | 113 | const formattedAppointmentStatusDistribution = appointmentStatusDistribution.map(({ status, _count }) => ({ 114 | status, 115 | count: Number(_count.id) 116 | })) 117 | 118 | return { 119 | appointmentCount, 120 | reviewCount, 121 | patientCount: patientCount.length, 122 | totalRevenue, 123 | formattedAppointmentStatusDistribution 124 | } 125 | } 126 | 127 | const getPatientMetaData = async (user: IAuthUser) => { 128 | const patientData = await prisma.patient.findUniqueOrThrow({ 129 | where: { 130 | email: user?.email 131 | } 132 | }); 133 | 134 | const appointmentCount = await prisma.appointment.count({ 135 | where: { 136 | patientId: patientData.id 137 | } 138 | }); 139 | 140 | const prescriptionCount = await prisma.prescription.count({ 141 | where: { 142 | patientId: patientData.id 143 | } 144 | }); 145 | 146 | const reviewCount = await prisma.review.count({ 147 | where: { 148 | patientId: patientData.id 149 | } 150 | }); 151 | 152 | const appointmentStatusDistribution = await prisma.appointment.groupBy({ 153 | by: ['status'], 154 | _count: { id: true }, 155 | where: { 156 | patientId: patientData.id 157 | } 158 | }); 159 | 160 | const formattedAppointmentStatusDistribution = appointmentStatusDistribution.map(({ status, _count }) => ({ 161 | status, 162 | count: Number(_count.id) 163 | })) 164 | 165 | return { 166 | appointmentCount, 167 | prescriptionCount, 168 | reviewCount, 169 | formattedAppointmentStatusDistribution 170 | } 171 | } 172 | 173 | const getBarChartData = async () => { 174 | const appointmentCountByMonth: { month: Date, count: bigint }[] = await prisma.$queryRaw` 175 | SELECT DATE_TRUNC('month', "createdAt") AS month, 176 | CAST(COUNT(*) AS INTEGER) AS count 177 | FROM "appointments" 178 | GROUP BY month 179 | ORDER BY month ASC 180 | ` 181 | 182 | return appointmentCountByMonth; 183 | }; 184 | 185 | const getPieChartData = async () => { 186 | const appointmentStatusDistribution = await prisma.appointment.groupBy({ 187 | by: ['status'], 188 | _count: { id: true } 189 | }); 190 | 191 | const formattedAppointmentStatusDistribution = appointmentStatusDistribution.map(({ status, _count }) => ({ 192 | status, 193 | count: Number(_count.id) 194 | })); 195 | 196 | return formattedAppointmentStatusDistribution; 197 | } 198 | 199 | export const MetaService = { 200 | fetchDashboardMetaData 201 | } -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.constants.ts: -------------------------------------------------------------------------------- 1 | export const patientSearchableFields: string[] = ['name', 'email', 'contactNo']; 2 | 3 | export const patientFilterableFields: string[] = [ 4 | 'searchTerm', 5 | 'email', 6 | 'contactNo', 7 | ]; 8 | -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import catchAsync from '../../../shared/catchAsync'; 3 | import sendResponse from '../../../shared/sendResponse'; 4 | import httpStatus from 'http-status'; 5 | import pick from '../../../shared/pick'; 6 | import { patientFilterableFields } from './patient.constants'; 7 | import { PatientService } from './patient.services'; 8 | 9 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 10 | const filters = pick(req.query, patientFilterableFields); 11 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 12 | 13 | const result = await PatientService.getAllFromDB(filters, options); 14 | 15 | sendResponse(res, { 16 | statusCode: httpStatus.OK, 17 | success: true, 18 | message: 'Patient retrieval successfully', 19 | meta: result.meta, 20 | data: result.data, 21 | }); 22 | }); 23 | 24 | const getByIdFromDB = catchAsync(async (req: Request, res: Response) => { 25 | 26 | const { id } = req.params; 27 | const result = await PatientService.getByIdFromDB(id); 28 | 29 | sendResponse(res, { 30 | statusCode: httpStatus.OK, 31 | success: true, 32 | message: 'Patient retrieval successfully', 33 | data: result, 34 | }); 35 | }); 36 | 37 | const updateIntoDB = catchAsync(async (req: Request, res: Response) => { 38 | const { id } = req.params; 39 | const result = await PatientService.updateIntoDB(id, req.body); 40 | 41 | sendResponse(res, { 42 | statusCode: httpStatus.OK, 43 | success: true, 44 | message: 'Patient updated successfully', 45 | data: result, 46 | }); 47 | }); 48 | 49 | const deleteFromDB = catchAsync(async (req: Request, res: Response) => { 50 | const { id } = req.params; 51 | const result = await PatientService.deleteFromDB(id); 52 | sendResponse(res, { 53 | statusCode: httpStatus.OK, 54 | success: true, 55 | message: 'Patient deleted successfully', 56 | data: result, 57 | }); 58 | }); 59 | 60 | 61 | const softDelete = catchAsync(async (req: Request, res: Response) => { 62 | const { id } = req.params; 63 | const result = await PatientService.softDelete(id); 64 | sendResponse(res, { 65 | statusCode: httpStatus.OK, 66 | success: true, 67 | message: 'Patient soft deleted successfully', 68 | data: result, 69 | }); 70 | }); 71 | 72 | export const PatientController = { 73 | getAllFromDB, 74 | getByIdFromDB, 75 | updateIntoDB, 76 | deleteFromDB, 77 | softDelete, 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.interface.ts: -------------------------------------------------------------------------------- 1 | import { BloodGroup, Gender, MaritalStatus } from "@prisma/client"; 2 | 3 | export type IPatientFilterRequest = { 4 | searchTerm?: string | undefined; 5 | email?: string | undefined; 6 | contactNo?: string | undefined; 7 | }; 8 | 9 | type IPatientHealthData = { 10 | gender: Gender 11 | dateOfBirth: string 12 | bloodGroup: BloodGroup 13 | hasAllergies?: boolean 14 | hasDiabetes?: boolean 15 | height: string 16 | weight: string 17 | smokingStatus?: boolean 18 | dietaryPreferences?: string 19 | pregnancyStatus?: boolean 20 | mentalHealthHistory?: string 21 | immunizationStatus?: string 22 | hasPastSurgeries?: boolean 23 | recentAnxiety?: boolean 24 | recentDepression?: boolean 25 | maritalStatus?: MaritalStatus 26 | } 27 | 28 | type IMedicalReport = { 29 | reportName: string 30 | reportLink: string 31 | } 32 | 33 | export type IPatientUpdate = { 34 | name: string 35 | contactNumber: string 36 | address: string; 37 | patientHealthData: IPatientHealthData, 38 | medicalReport: IMedicalReport 39 | } -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { PatientController } from './patient.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get( 7 | '/', 8 | PatientController.getAllFromDB 9 | ); 10 | 11 | router.get( 12 | '/:id', 13 | PatientController.getByIdFromDB 14 | ); 15 | 16 | router.patch( 17 | '/:id', 18 | PatientController.updateIntoDB 19 | ); 20 | 21 | router.delete( 22 | '/:id', 23 | PatientController.deleteFromDB 24 | ); 25 | router.delete( 26 | '/soft/:id', 27 | PatientController.softDelete 28 | ); 29 | 30 | export const PatientRoutes = router; 31 | -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.services.ts: -------------------------------------------------------------------------------- 1 | import { Patient, Prisma, UserStatus } from '@prisma/client'; 2 | import prisma from '../../../shared/prisma'; 3 | import { IPatientFilterRequest, IPatientUpdate } from './patient.interface'; 4 | import { IPaginationOptions } from '../../interfaces/pagination'; 5 | import { paginationHelper } from '../../../helpars/paginationHelper'; 6 | import { patientSearchableFields } from './patient.constants'; 7 | 8 | const getAllFromDB = async ( 9 | filters: IPatientFilterRequest, 10 | options: IPaginationOptions, 11 | ) => { 12 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 13 | const { searchTerm, ...filterData } = filters; 14 | 15 | const andConditions = []; 16 | 17 | if (searchTerm) { 18 | andConditions.push({ 19 | OR: patientSearchableFields.map(field => ({ 20 | [field]: { 21 | contains: searchTerm, 22 | mode: 'insensitive', 23 | }, 24 | })), 25 | }); 26 | } 27 | 28 | if (Object.keys(filterData).length > 0) { 29 | andConditions.push({ 30 | AND: Object.keys(filterData).map(key => { 31 | return { 32 | [key]: { 33 | equals: (filterData as any)[key], 34 | }, 35 | }; 36 | }), 37 | }); 38 | } 39 | andConditions.push({ 40 | isDeleted: false, 41 | }); 42 | 43 | const whereConditions: Prisma.PatientWhereInput = 44 | andConditions.length > 0 ? { AND: andConditions } : {}; 45 | 46 | const result = await prisma.patient.findMany({ 47 | where: whereConditions, 48 | skip, 49 | take: limit, 50 | orderBy: 51 | options.sortBy && options.sortOrder 52 | ? { [options.sortBy]: options.sortOrder } 53 | : { 54 | createdAt: 'desc', 55 | }, 56 | include: { 57 | medicalReport: true, 58 | patientHealthData: true, 59 | } 60 | }); 61 | const total = await prisma.patient.count({ 62 | where: whereConditions, 63 | }); 64 | 65 | return { 66 | meta: { 67 | total, 68 | page, 69 | limit, 70 | }, 71 | data: result, 72 | }; 73 | }; 74 | 75 | const getByIdFromDB = async (id: string): Promise => { 76 | const result = await prisma.patient.findUnique({ 77 | where: { 78 | id, 79 | isDeleted: false, 80 | }, 81 | include: { 82 | medicalReport: true, 83 | patientHealthData: true, 84 | }, 85 | }); 86 | return result; 87 | }; 88 | 89 | const updateIntoDB = async (id: string, payload: Partial): Promise => { 90 | 91 | const { patientHealthData, medicalReport, ...patientData } = payload; 92 | 93 | const patientInfo = await prisma.patient.findUniqueOrThrow({ 94 | where: { 95 | id, 96 | isDeleted: false 97 | } 98 | }); 99 | 100 | await prisma.$transaction(async (transactionClient) => { 101 | //update patient data 102 | await transactionClient.patient.update({ 103 | where: { 104 | id 105 | }, 106 | data: patientData, 107 | include: { 108 | patientHealthData: true, 109 | medicalReport: true 110 | } 111 | }); 112 | 113 | // create or update patient health data 114 | if (patientHealthData) { 115 | await transactionClient.patientHealthData.upsert({ 116 | where: { 117 | patientId: patientInfo.id 118 | }, 119 | update: patientHealthData, 120 | create: { ...patientHealthData, patientId: patientInfo.id } 121 | }); 122 | }; 123 | 124 | if (medicalReport) { 125 | await transactionClient.medicalReport.create({ 126 | data: { ...medicalReport, patientId: patientInfo.id } 127 | }) 128 | } 129 | }) 130 | 131 | 132 | const responseData = await prisma.patient.findUnique({ 133 | where: { 134 | id: patientInfo.id 135 | }, 136 | include: { 137 | patientHealthData: true, 138 | medicalReport: true 139 | } 140 | }) 141 | return responseData; 142 | 143 | }; 144 | 145 | 146 | const deleteFromDB = async (id: string): Promise => { 147 | const result = await prisma.$transaction(async (tx) => { 148 | // delete medical report 149 | await tx.medicalReport.deleteMany({ 150 | where: { 151 | patientId: id 152 | } 153 | }); 154 | 155 | // delete patient health data 156 | await tx.patientHealthData.delete({ 157 | where: { 158 | patientId: id 159 | } 160 | }); 161 | 162 | const deletedPatient = await tx.patient.delete({ 163 | where: { 164 | id 165 | } 166 | }); 167 | 168 | await tx.user.delete({ 169 | where: { 170 | email: deletedPatient.email 171 | } 172 | }); 173 | 174 | return deletedPatient; 175 | }); 176 | 177 | return result; 178 | }; 179 | 180 | const softDelete = async (id: string): Promise => { 181 | return await prisma.$transaction(async transactionClient => { 182 | const deletedPatient = await transactionClient.patient.update({ 183 | where: { id }, 184 | data: { 185 | isDeleted: true, 186 | }, 187 | }); 188 | 189 | await transactionClient.user.update({ 190 | where: { 191 | email: deletedPatient.email, 192 | }, 193 | data: { 194 | status: UserStatus.DELETED, 195 | }, 196 | }); 197 | 198 | return deletedPatient; 199 | }); 200 | }; 201 | 202 | export const PatientService = { 203 | getAllFromDB, 204 | getByIdFromDB, 205 | updateIntoDB, 206 | deleteFromDB, 207 | softDelete, 208 | }; 209 | -------------------------------------------------------------------------------- /src/app/modules/Patient/patient.validation.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apollo-Level2-Web-Dev/PH-HealthCare-Server/38d78af6aef6aeb2dd454e3866c3f449012337e2/src/app/modules/Patient/patient.validation.ts -------------------------------------------------------------------------------- /src/app/modules/Payment/payment.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import { PaymentService } from "./payment.service"; 4 | import sendResponse from "../../../shared/sendResponse"; 5 | import httpStatus from "http-status"; 6 | 7 | const initPayment = catchAsync(async (req: Request, res: Response) => { 8 | const { appointmentId } = req.params; 9 | const result = await PaymentService.initPayment(appointmentId); 10 | sendResponse(res, { 11 | statusCode: httpStatus.OK, 12 | success: true, 13 | message: 'Payment initiate successfully', 14 | data: result, 15 | }); 16 | }); 17 | 18 | const validatePayment = catchAsync(async (req: Request, res: Response) => { 19 | const result = await PaymentService.validatePayment(req.query); 20 | sendResponse(res, { 21 | statusCode: httpStatus.OK, 22 | success: true, 23 | message: 'Payment validate successfully', 24 | data: result, 25 | }); 26 | }); 27 | 28 | export const PaymentController = { 29 | initPayment, 30 | validatePayment 31 | } -------------------------------------------------------------------------------- /src/app/modules/Payment/payment.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { PaymentController } from './payment.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get( 7 | '/ipn', 8 | PaymentController.validatePayment 9 | ) 10 | 11 | router.post( 12 | '/init-payment/:appointmentId', 13 | PaymentController.initPayment 14 | ) 15 | 16 | export const PaymentRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Payment/payment.service.ts: -------------------------------------------------------------------------------- 1 | import prisma from '../../../shared/prisma'; 2 | import { SSLService } from '../SSL/ssl.service'; 3 | import { PaymentStatus } from '@prisma/client'; 4 | 5 | const initPayment = async (appointmentId: string) => { 6 | const paymentData = await prisma.payment.findFirstOrThrow({ 7 | where: { 8 | appointmentId 9 | }, 10 | include: { 11 | appointment: { 12 | include: { 13 | patient: true 14 | } 15 | } 16 | } 17 | }); 18 | 19 | const initPaymentData = { 20 | amount: paymentData.amount, 21 | transactionId: paymentData.transactionId, 22 | name: paymentData.appointment.patient.name, 23 | email: paymentData.appointment.patient.email, 24 | address: paymentData.appointment.patient.address, 25 | phoneNumber: paymentData.appointment.patient.contactNumber 26 | } 27 | 28 | const result = await SSLService.initPayment(initPaymentData); 29 | return { 30 | paymentUrl: result.GatewayPageURL 31 | }; 32 | 33 | }; 34 | 35 | // ssl commerz ipn listener query 36 | // amount=1150.00&bank_tran_id=151114130739MqCBNx5&card_brand=VISA&card_issuer=BRAC+BANK%2C+LTD.&card_issuer_country=Bangladesh&card_issuer_country_code=BD&card_no=432149XXXXXX0667&card_type=VISA-Brac+bank¤cy=BDT&status=VALID&store_amount=1104.00&store_id=progr6606bdd704623&tran_date=2015-11-14+13%3A07%3A12&tran_id=5646dd9d4b484&val_id=151114130742Bj94IBUk4uE5GRj&verify_sign=490d86b8ac5faa016f695b60972a7fac&verify_key=amount%2Cbank_tran_id%2Ccard_brand%2Ccard_issuer%2Ccard_issuer_country%2Ccard_issuer_country_code%2Ccard_no%2Ccard_type%2Ccurrency%2Cstatus%2Cstore_amount%2Cstore_id%2Ctran_date%2Ctran_id%2Cval_id 37 | 38 | const validatePayment = async (payload: any) => { 39 | // if (!payload || !payload.status || !(payload.status === 'VALID')) { 40 | // return { 41 | // message: "Invalid Payment!" 42 | // } 43 | // } 44 | 45 | // const response = await SSLService.validatePayment(payload); 46 | 47 | // if (response?.status !== 'VALID') { 48 | // return { 49 | // message: "Payment Failed!" 50 | // } 51 | // } 52 | 53 | const response = payload; 54 | 55 | await prisma.$transaction(async (tx) => { 56 | const updatedPaymentData = await tx.payment.update({ 57 | where: { 58 | transactionId: response.tran_id 59 | }, 60 | data: { 61 | status: PaymentStatus.PAID, 62 | paymentGatewayData: response 63 | } 64 | }); 65 | 66 | await tx.appointment.update({ 67 | where: { 68 | id: updatedPaymentData.appointmentId 69 | }, 70 | data: { 71 | paymentStatus: PaymentStatus.PAID 72 | } 73 | }) 74 | }); 75 | 76 | return { 77 | message: "Payment success!" 78 | } 79 | 80 | } 81 | 82 | export const PaymentService = { 83 | initPayment, 84 | validatePayment 85 | } -------------------------------------------------------------------------------- /src/app/modules/Prescription/prescription.constants.ts: -------------------------------------------------------------------------------- 1 | export const prescriptionFilterableFields: string[] = [ 2 | 'patientEmail', 3 | 'doctorEmail', 4 | ]; -------------------------------------------------------------------------------- /src/app/modules/Prescription/prescription.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import catchAsync from '../../../shared/catchAsync'; 3 | import sendResponse from '../../../shared/sendResponse'; 4 | import httpStatus from 'http-status'; 5 | import { PrescriptionService } from './prescription.service'; 6 | import { IAuthUser } from '../../interfaces/common'; 7 | import pick from '../../../shared/pick'; 8 | import { prescriptionFilterableFields } from './prescription.constants'; 9 | 10 | const insertIntoDB = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 11 | const user = req.user; 12 | const result = await PrescriptionService.insertIntoDB(user as IAuthUser, req.body); 13 | sendResponse(res, { 14 | statusCode: httpStatus.OK, 15 | success: true, 16 | message: 'Prescription created successfully', 17 | data: result, 18 | }); 19 | }); 20 | 21 | const patientPrescription = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 22 | const user = req.user; 23 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']) 24 | const result = await PrescriptionService.patientPrescription(user as IAuthUser, options); 25 | sendResponse(res, { 26 | statusCode: httpStatus.OK, 27 | success: true, 28 | message: 'Prescription fetched successfully', 29 | meta: result.meta, 30 | data: result.data 31 | }); 32 | }); 33 | 34 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 35 | const filters = pick(req.query, prescriptionFilterableFields); 36 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 37 | const result = await PrescriptionService.getAllFromDB(filters, options); 38 | sendResponse(res, { 39 | statusCode: httpStatus.OK, 40 | success: true, 41 | message: 'Prescriptions retrieval successfully', 42 | meta: result.meta, 43 | data: result.data, 44 | }); 45 | }); 46 | 47 | export const PrescriptionController = { 48 | insertIntoDB, 49 | patientPrescription, 50 | getAllFromDB 51 | }; 52 | -------------------------------------------------------------------------------- /src/app/modules/Prescription/prescription.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { PrescriptionController } from './prescription.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import validateRequest from '../../middlewares/validateRequest'; 6 | import { PrescriptionValidation } from './prescription.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get( 11 | '/', 12 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 13 | PrescriptionController.getAllFromDB 14 | ); 15 | 16 | router.get( 17 | '/my-prescription', 18 | auth(UserRole.PATIENT), 19 | PrescriptionController.patientPrescription 20 | ) 21 | 22 | router.post( 23 | '/', 24 | auth(UserRole.DOCTOR), 25 | validateRequest(PrescriptionValidation.create), 26 | PrescriptionController.insertIntoDB 27 | ) 28 | 29 | 30 | export const PrescriptionRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Prescription/prescription.service.ts: -------------------------------------------------------------------------------- 1 | import { AppointmentStatus, PaymentStatus, Prescription, Prisma } from "@prisma/client" 2 | import prisma from "../../../shared/prisma" 3 | import { IAuthUser } from "../../interfaces/common" 4 | import ApiError from "../../errors/ApiError"; 5 | import httpStatus from "http-status"; 6 | import { IPaginationOptions } from "../../interfaces/pagination"; 7 | import { paginationHelper } from "../../../helpars/paginationHelper"; 8 | 9 | const insertIntoDB = async (user: IAuthUser, payload: Partial) => { 10 | const appointmentData = await prisma.appointment.findUniqueOrThrow({ 11 | where: { 12 | id: payload.appointmentId, 13 | status: AppointmentStatus.COMPLETED, 14 | paymentStatus: PaymentStatus.PAID 15 | }, 16 | include: { 17 | doctor: true 18 | } 19 | }); 20 | 21 | if (!(user?.email === appointmentData.doctor.email)) { 22 | throw new ApiError(httpStatus.BAD_REQUEST, "This is not your appointment!") 23 | }; 24 | 25 | const result = await prisma.prescription.create({ 26 | data: { 27 | appointmentId: appointmentData.id, 28 | doctorId: appointmentData.doctorId, 29 | patientId: appointmentData.patientId, 30 | instructions: payload.instructions as string, 31 | followUpDate: payload.followUpDate || null || undefined 32 | }, 33 | include: { 34 | patient: true 35 | } 36 | }); 37 | 38 | 39 | return result; 40 | }; 41 | 42 | const patientPrescription = async (user: IAuthUser, options: IPaginationOptions) => { 43 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 44 | 45 | const result = await prisma.prescription.findMany({ 46 | where: { 47 | patient: { 48 | email: user?.email 49 | } 50 | }, 51 | skip, 52 | take: limit, 53 | orderBy: options.sortBy && options.sortOrder 54 | ? { [options.sortBy]: options.sortOrder } 55 | : { createdAt: 'desc' }, 56 | include: { 57 | doctor: true, 58 | patient: true, 59 | appointment: true 60 | } 61 | }); 62 | 63 | const total = await prisma.prescription.count({ 64 | where: { 65 | patient: { 66 | email: user?.email 67 | } 68 | } 69 | }) 70 | 71 | return { 72 | meta: { 73 | total, 74 | page, 75 | limit 76 | }, 77 | data: result 78 | }; 79 | }; 80 | 81 | const getAllFromDB = async ( 82 | filters: any, 83 | options: IPaginationOptions, 84 | ) => { 85 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 86 | const { patientEmail, doctorEmail } = filters; 87 | const andConditions = []; 88 | 89 | if (patientEmail) { 90 | andConditions.push({ 91 | patient: { 92 | email: patientEmail 93 | } 94 | }) 95 | } 96 | 97 | if (doctorEmail) { 98 | andConditions.push({ 99 | doctor: { 100 | email: doctorEmail 101 | } 102 | }) 103 | } 104 | 105 | const whereConditions: Prisma.PrescriptionWhereInput = 106 | andConditions.length > 0 ? { AND: andConditions } : {}; 107 | 108 | const result = await prisma.prescription.findMany({ 109 | where: whereConditions, 110 | skip, 111 | take: limit, 112 | orderBy: 113 | options.sortBy && options.sortOrder 114 | ? { [options.sortBy]: options.sortOrder } 115 | : { 116 | createdAt: 'desc', 117 | }, 118 | include: { 119 | doctor: true, 120 | patient: true, 121 | appointment: true, 122 | }, 123 | }); 124 | const total = await prisma.prescription.count({ 125 | where: whereConditions, 126 | }); 127 | 128 | return { 129 | meta: { 130 | total, 131 | page, 132 | limit, 133 | }, 134 | data: result, 135 | }; 136 | }; 137 | 138 | 139 | export const PrescriptionService = { 140 | insertIntoDB, 141 | patientPrescription, 142 | getAllFromDB 143 | } -------------------------------------------------------------------------------- /src/app/modules/Prescription/prescription.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const create = z.object({ 4 | body: z.object({ 5 | appointmentId: z.string({ 6 | required_error: 'Appointment Id is required', 7 | }), 8 | instructions: z.string({ 9 | required_error: 'Instructions is required', 10 | }), 11 | }), 12 | }); 13 | 14 | export const PrescriptionValidation = { 15 | create, 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/modules/Review/review.contant.ts: -------------------------------------------------------------------------------- 1 | export const reviewFilterableFields: string[] = ['patientEmail', 'doctorEmail']; -------------------------------------------------------------------------------- /src/app/modules/Review/review.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import sendResponse from "../../../shared/sendResponse"; 4 | import httpStatus from "http-status"; 5 | import { ReviewService } from "./review.service"; 6 | import { IAuthUser } from "../../interfaces/common"; 7 | import pick from "../../../shared/pick"; 8 | import { reviewFilterableFields } from "./review.contant"; 9 | 10 | const insertIntoDB = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 11 | const user = req.user; 12 | const result = await ReviewService.insertIntoDB(user as IAuthUser, req.body); 13 | sendResponse(res, { 14 | statusCode: httpStatus.OK, 15 | success: true, 16 | message: 'Review created successfully', 17 | data: result, 18 | }); 19 | }); 20 | 21 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 22 | const filters = pick(req.query, reviewFilterableFields); 23 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 24 | const result = await ReviewService.getAllFromDB(filters, options); 25 | sendResponse(res, { 26 | statusCode: httpStatus.OK, 27 | success: true, 28 | message: 'Reviews retrieval successfully', 29 | meta: result.meta, 30 | data: result.data, 31 | }); 32 | }); 33 | 34 | 35 | export const ReviewController = { 36 | insertIntoDB, 37 | getAllFromDB 38 | } -------------------------------------------------------------------------------- /src/app/modules/Review/review.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { ReviewController } from './review.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import validateRequest from '../../middlewares/validateRequest'; 6 | import { ReviewValidation } from './review.validation'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/', ReviewController.getAllFromDB); 11 | 12 | router.post( 13 | '/', 14 | auth(UserRole.PATIENT), 15 | validateRequest(ReviewValidation.create), 16 | ReviewController.insertIntoDB 17 | ); 18 | 19 | 20 | export const ReviewRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Review/review.service.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from "http-status"; 2 | import prisma from "../../../shared/prisma" 3 | import ApiError from "../../errors/ApiError"; 4 | import { IAuthUser } from "../../interfaces/common" 5 | import { IPaginationOptions } from "../../interfaces/pagination"; 6 | import { paginationHelper } from "../../../helpars/paginationHelper"; 7 | import { Prisma } from "@prisma/client"; 8 | 9 | const insertIntoDB = async (user: IAuthUser, payload: any) => { 10 | const patientData = await prisma.patient.findUniqueOrThrow({ 11 | where: { 12 | email: user?.email 13 | } 14 | }); 15 | 16 | const appointmentData = await prisma.appointment.findUniqueOrThrow({ 17 | where: { 18 | id: payload.appointmentId 19 | } 20 | }); 21 | 22 | if (!(patientData.id === appointmentData.patientId)) { 23 | throw new ApiError(httpStatus.BAD_REQUEST, "This is not your appointment!") 24 | } 25 | 26 | return await prisma.$transaction(async (tx) => { 27 | const result = await tx.review.create({ 28 | data: { 29 | appointmentId: appointmentData.id, 30 | doctorId: appointmentData.doctorId, 31 | patientId: appointmentData.patientId, 32 | rating: payload.rating, 33 | comment: payload.comment 34 | } 35 | }); 36 | 37 | const averageRating = await tx.review.aggregate({ 38 | _avg: { 39 | rating: true 40 | } 41 | }); 42 | 43 | await tx.doctor.update({ 44 | where: { 45 | id: result.doctorId 46 | }, 47 | data: { 48 | averageRating: averageRating._avg.rating as number 49 | } 50 | }) 51 | 52 | return result; 53 | }) 54 | }; 55 | 56 | const getAllFromDB = async ( 57 | filters: any, 58 | options: IPaginationOptions, 59 | ) => { 60 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 61 | const { patientEmail, doctorEmail } = filters; 62 | const andConditions = []; 63 | 64 | if (patientEmail) { 65 | andConditions.push({ 66 | patient: { 67 | email: patientEmail 68 | } 69 | }) 70 | } 71 | 72 | if (doctorEmail) { 73 | andConditions.push({ 74 | doctor: { 75 | email: doctorEmail 76 | } 77 | }) 78 | } 79 | 80 | const whereConditions: Prisma.ReviewWhereInput = 81 | andConditions.length > 0 ? { AND: andConditions } : {}; 82 | 83 | const result = await prisma.review.findMany({ 84 | where: whereConditions, 85 | skip, 86 | take: limit, 87 | orderBy: 88 | options.sortBy && options.sortOrder 89 | ? { [options.sortBy]: options.sortOrder } 90 | : { 91 | createdAt: 'desc', 92 | }, 93 | include: { 94 | doctor: true, 95 | patient: true, 96 | //appointment: true, 97 | }, 98 | }); 99 | const total = await prisma.review.count({ 100 | where: whereConditions, 101 | }); 102 | 103 | return { 104 | meta: { 105 | total, 106 | page, 107 | limit, 108 | }, 109 | data: result, 110 | }; 111 | }; 112 | 113 | export const ReviewService = { 114 | insertIntoDB, 115 | getAllFromDB 116 | } -------------------------------------------------------------------------------- /src/app/modules/Review/review.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const create = z.object({ 4 | body: z.object({ 5 | appointmentId: z.string({ 6 | required_error: 'Appointment Id is required', 7 | }), 8 | rating: z.number({ 9 | required_error: 'Rating is required', 10 | }), 11 | comment: z.string({ 12 | required_error: 'Comment is required', 13 | }) 14 | }), 15 | }); 16 | 17 | export const ReviewValidation = { 18 | create, 19 | }; -------------------------------------------------------------------------------- /src/app/modules/SSL/ssl.interface.ts: -------------------------------------------------------------------------------- 1 | export type IPaymentData = { 2 | amount: number; 3 | transactionId: string; 4 | name: string; 5 | email: string; 6 | address: string | null; 7 | phoneNumber: string | null; 8 | } -------------------------------------------------------------------------------- /src/app/modules/SSL/ssl.service.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import config from "../../../config"; 3 | import ApiError from "../../errors/ApiError"; 4 | import httpStatus from "http-status"; 5 | import { IPaymentData } from "./ssl.interface"; 6 | 7 | const initPayment = async (paymentData: IPaymentData) => { 8 | try { 9 | const data = { 10 | store_id: config.ssl.storeId, 11 | store_passwd: config.ssl.storePass, 12 | total_amount: paymentData.amount, 13 | currency: 'BDT', 14 | tran_id: paymentData.transactionId, // use unique tran_id for each api call 15 | success_url: config.ssl.successUrl, 16 | fail_url: config.ssl.failUrl, 17 | cancel_url: config.ssl.cancelUrl, 18 | ipn_url: 'http://localhost:3030/ipn', 19 | shipping_method: 'N/A', 20 | product_name: 'Appointment', 21 | product_category: 'Service', 22 | product_profile: 'general', 23 | cus_name: paymentData.name, 24 | cus_email: paymentData.email, 25 | cus_add1: paymentData.address, 26 | cus_add2: 'N/A', 27 | cus_city: 'Dhaka', 28 | cus_state: 'Dhaka', 29 | cus_postcode: '1000', 30 | cus_country: 'Bangladesh', 31 | cus_phone: paymentData.phoneNumber, 32 | cus_fax: '01711111111', 33 | ship_name: 'N/A', 34 | ship_add1: 'N/A', 35 | ship_add2: 'N/A', 36 | ship_city: 'N/A', 37 | ship_state: 'N/A', 38 | ship_postcode: 1000, 39 | ship_country: 'N/A', 40 | }; 41 | 42 | const response = await axios({ 43 | method: 'post', 44 | url: config.ssl.sslPaymentApi, 45 | data: data, 46 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' } 47 | }); 48 | 49 | return response.data; 50 | } 51 | catch (err) { 52 | throw new ApiError(httpStatus.BAD_REQUEST, "Payment erro occured!") 53 | } 54 | }; 55 | 56 | 57 | const validatePayment = async (payload: any) => { 58 | try { 59 | const response = await axios({ 60 | method: 'GET', 61 | url: `${config.ssl.sslValidationApi}?val_id=${payload.val_id}&store_id=${config.ssl.storeId}&store_passwd=${config.ssl.storePass}&format=json` 62 | }); 63 | 64 | return response.data; 65 | } 66 | catch (err) { 67 | throw new ApiError(httpStatus.BAD_REQUEST, "Payment validation failed!") 68 | } 69 | } 70 | 71 | 72 | export const SSLService = { 73 | initPayment, 74 | validatePayment 75 | } -------------------------------------------------------------------------------- /src/app/modules/Schedule/schedule.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import sendResponse from "../../../shared/sendResponse"; 4 | import httpStatus from "http-status"; 5 | import { ScheduleService } from "./schedule.sevice"; 6 | import pick from "../../../shared/pick"; 7 | import { IAuthUser } from "../../interfaces/common"; 8 | 9 | const inserIntoDB = catchAsync(async (req: Request, res: Response) => { 10 | const result = await ScheduleService.inserIntoDB(req.body); 11 | 12 | sendResponse(res, { 13 | statusCode: httpStatus.OK, 14 | success: true, 15 | message: "Schedule created successfully!", 16 | data: result 17 | }); 18 | }); 19 | 20 | const getAllFromDB = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 21 | const filters = pick(req.query, ['startDate', 'endDate']); 22 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']); 23 | 24 | const user = req.user; 25 | const result = await ScheduleService.getAllFromDB(filters, options, user as IAuthUser); 26 | 27 | sendResponse(res, { 28 | statusCode: httpStatus.OK, 29 | success: true, 30 | message: "Schedule fetched successfully!", 31 | data: result 32 | }); 33 | }); 34 | 35 | const getByIdFromDB = catchAsync(async (req: Request, res: Response) => { 36 | const { id } = req.params; 37 | const result = await ScheduleService.getByIdFromDB(id); 38 | sendResponse(res, { 39 | statusCode: httpStatus.OK, 40 | success: true, 41 | message: 'Schedule retrieval successfully', 42 | data: result, 43 | }); 44 | }); 45 | 46 | const deleteFromDB = catchAsync(async (req: Request, res: Response) => { 47 | const { id } = req.params; 48 | const result = await ScheduleService.deleteFromDB(id); 49 | sendResponse(res, { 50 | statusCode: httpStatus.OK, 51 | success: true, 52 | message: 'Schedule deleted successfully', 53 | data: result, 54 | }); 55 | }); 56 | 57 | 58 | export const ScheduleController = { 59 | inserIntoDB, 60 | getAllFromDB, 61 | getByIdFromDB, 62 | deleteFromDB 63 | }; -------------------------------------------------------------------------------- /src/app/modules/Schedule/schedule.interface.ts: -------------------------------------------------------------------------------- 1 | export type ISchedule = { 2 | startDate: string; 3 | endDate: string; 4 | startTime: string; 5 | endTime: string; 6 | } 7 | 8 | export type IFilterRequest = { 9 | startDate?: string | undefined; 10 | endDate?: string | undefined; 11 | } -------------------------------------------------------------------------------- /src/app/modules/Schedule/schedule.routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ScheduleController } from './schedule.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | 6 | const router = express.Router(); 7 | 8 | router.get( 9 | '/', 10 | auth(UserRole.DOCTOR), 11 | ScheduleController.getAllFromDB 12 | ); 13 | 14 | /** 15 | * API ENDPOINT: /schedule/:id 16 | * 17 | * Get schedule data by id 18 | */ 19 | router.get( 20 | '/:id', 21 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR), 22 | ScheduleController.getByIdFromDB 23 | ); 24 | 25 | router.post( 26 | '/', 27 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 28 | ScheduleController.inserIntoDB 29 | ); 30 | 31 | 32 | 33 | /** 34 | * API ENDPOINT: /schdeule/:id 35 | * 36 | * Delete schedule data by id 37 | */ 38 | router.delete( 39 | '/:id', 40 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 41 | ScheduleController.deleteFromDB 42 | ); 43 | 44 | export const ScheduleRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Schedule/schedule.sevice.ts: -------------------------------------------------------------------------------- 1 | import { addHours, addMinutes, format } from 'date-fns'; 2 | import prisma from '../../../shared/prisma'; 3 | import { Prisma, Schedule } from '@prisma/client'; 4 | import { IFilterRequest, ISchedule } from './schedule.interface'; 5 | import { IPaginationOptions } from '../../interfaces/pagination'; 6 | import { paginationHelper } from '../../../helpars/paginationHelper'; 7 | import { IAuthUser } from '../../interfaces/common'; 8 | 9 | const convertDateTime = async (date: Date) => { 10 | const offset = date.getTimezoneOffset() * 60000; 11 | return new Date(date.getTime() + offset); 12 | } 13 | 14 | const inserIntoDB = async (payload: ISchedule): Promise => { 15 | const { startDate, endDate, startTime, endTime } = payload; 16 | 17 | const interverlTime = 30; 18 | 19 | const schedules = []; 20 | 21 | const currentDate = new Date(startDate); // start date 22 | const lastDate = new Date(endDate) // end date 23 | 24 | while (currentDate <= lastDate) { 25 | // 09:30 ---> ['09', '30'] 26 | const startDateTime = new Date( 27 | addMinutes( 28 | addHours( 29 | `${format(currentDate, 'yyyy-MM-dd')}`, 30 | Number(startTime.split(':')[0]) 31 | ), 32 | Number(startTime.split(':')[1]) 33 | ) 34 | ); 35 | 36 | const endDateTime = new Date( 37 | addMinutes( 38 | addHours( 39 | `${format(currentDate, 'yyyy-MM-dd')}`, 40 | Number(endTime.split(':')[0]) 41 | ), 42 | Number(endTime.split(':')[1]) 43 | ) 44 | ); 45 | 46 | while (startDateTime < endDateTime) { 47 | // const scheduleData = { 48 | // startDateTime: startDateTime, 49 | // endDateTime: addMinutes(startDateTime, interverlTime) 50 | // } 51 | 52 | const s = await convertDateTime(startDateTime); 53 | const e = await convertDateTime(addMinutes(startDateTime, interverlTime)) 54 | 55 | const scheduleData = { 56 | startDateTime: s, 57 | endDateTime: e 58 | } 59 | 60 | const existingSchedule = await prisma.schedule.findFirst({ 61 | where: { 62 | startDateTime: scheduleData.startDateTime, 63 | endDateTime: scheduleData.endDateTime 64 | } 65 | }); 66 | 67 | if (!existingSchedule) { 68 | const result = await prisma.schedule.create({ 69 | data: scheduleData 70 | }); 71 | schedules.push(result); 72 | } 73 | 74 | startDateTime.setMinutes(startDateTime.getMinutes() + interverlTime); 75 | } 76 | 77 | currentDate.setDate(currentDate.getDate() + 1); 78 | } 79 | 80 | return schedules; 81 | }; 82 | 83 | const getAllFromDB = async ( 84 | filters: IFilterRequest, 85 | options: IPaginationOptions, 86 | user: IAuthUser 87 | ) => { 88 | const { limit, page, skip } = paginationHelper.calculatePagination(options); 89 | const { startDate, endDate, ...filterData } = filters; 90 | 91 | const andConditions = []; 92 | 93 | if (startDate && endDate) { 94 | andConditions.push({ 95 | AND: [ 96 | { 97 | startDateTime: { 98 | gte: startDate 99 | } 100 | }, 101 | { 102 | endDateTime: { 103 | lte: endDate 104 | } 105 | } 106 | ] 107 | }) 108 | }; 109 | 110 | 111 | if (Object.keys(filterData).length > 0) { 112 | andConditions.push({ 113 | AND: Object.keys(filterData).map(key => { 114 | return { 115 | [key]: { 116 | equals: (filterData as any)[key], 117 | }, 118 | }; 119 | }), 120 | }); 121 | } 122 | 123 | const whereConditions: Prisma.ScheduleWhereInput = 124 | andConditions.length > 0 ? { AND: andConditions } : {}; 125 | 126 | const doctorSchedules = await prisma.doctorSchedules.findMany({ 127 | where: { 128 | doctor: { 129 | email: user?.email 130 | } 131 | } 132 | }); 133 | 134 | const doctorScheduleIds = doctorSchedules.map(schedule => schedule.scheduleId); 135 | console.log(doctorScheduleIds) 136 | 137 | const result = await prisma.schedule.findMany({ 138 | where: { 139 | ...whereConditions, 140 | id: { 141 | notIn: doctorScheduleIds 142 | } 143 | }, 144 | skip, 145 | take: limit, 146 | orderBy: 147 | options.sortBy && options.sortOrder 148 | ? { [options.sortBy]: options.sortOrder } 149 | : { 150 | createdAt: 'desc', 151 | } 152 | }); 153 | const total = await prisma.schedule.count({ 154 | where: { 155 | ...whereConditions, 156 | id: { 157 | notIn: doctorScheduleIds 158 | } 159 | }, 160 | }); 161 | 162 | return { 163 | meta: { 164 | total, 165 | page, 166 | limit, 167 | }, 168 | data: result, 169 | }; 170 | }; 171 | 172 | const getByIdFromDB = async (id: string): Promise => { 173 | const result = await prisma.schedule.findUnique({ 174 | where: { 175 | id, 176 | }, 177 | }); 178 | //console.log(result?.startDateTime.getHours() + ":" + result?.startDateTime.getMinutes()) 179 | return result; 180 | }; 181 | 182 | const deleteFromDB = async (id: string): Promise => { 183 | const result = await prisma.schedule.delete({ 184 | where: { 185 | id, 186 | }, 187 | }); 188 | return result; 189 | }; 190 | 191 | 192 | export const ScheduleService = { 193 | inserIntoDB, 194 | getAllFromDB, 195 | getByIdFromDB, 196 | deleteFromDB 197 | } -------------------------------------------------------------------------------- /src/app/modules/Specialties/specialties.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import catchAsync from "../../../shared/catchAsync"; 3 | import sendResponse from "../../../shared/sendResponse"; 4 | import httpStatus from "http-status"; 5 | import { SpecialtiesService } from "./specialties.service"; 6 | 7 | const inserIntoDB = catchAsync(async (req: Request, res: Response) => { 8 | console.log(req.body) 9 | const result = await SpecialtiesService.inserIntoDB(req); 10 | 11 | sendResponse(res, { 12 | statusCode: httpStatus.OK, 13 | success: true, 14 | message: "Specialties created successfully!", 15 | data: result 16 | }); 17 | }); 18 | 19 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 20 | const result = await SpecialtiesService.getAllFromDB(); 21 | sendResponse(res, { 22 | statusCode: httpStatus.OK, 23 | success: true, 24 | message: 'Specialties data fetched successfully', 25 | data: result, 26 | }); 27 | }); 28 | 29 | const deleteFromDB = catchAsync(async (req: Request, res: Response) => { 30 | const { id } = req.params; 31 | const result = await SpecialtiesService.deleteFromDB(id); 32 | sendResponse(res, { 33 | statusCode: httpStatus.OK, 34 | success: true, 35 | message: 'Specialty deleted successfully', 36 | data: result, 37 | }); 38 | }); 39 | 40 | export const SpecialtiesController = { 41 | inserIntoDB, 42 | getAllFromDB, 43 | deleteFromDB 44 | }; -------------------------------------------------------------------------------- /src/app/modules/Specialties/specialties.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import { SpecialtiesController } from './specialties.controller'; 3 | import { fileUploader } from '../../../helpars/fileUploader'; 4 | import { SpecialtiesValidtaion } from './specialties.validation'; 5 | import auth from '../../middlewares/auth'; 6 | import { UserRole } from '@prisma/client'; 7 | 8 | 9 | const router = express.Router(); 10 | 11 | 12 | // Task 1: Retrieve Specialties Data 13 | 14 | /** 15 | - Develop an API endpoint to retrieve all specialties data. 16 | - Implement an HTTP GET endpoint returning specialties in JSON format. 17 | - ENDPOINT: /specialties 18 | */ 19 | router.get( 20 | '/', 21 | SpecialtiesController.getAllFromDB 22 | ); 23 | 24 | router.post( 25 | '/', 26 | fileUploader.upload.single('file'), 27 | (req: Request, res: Response, next: NextFunction) => { 28 | req.body = SpecialtiesValidtaion.create.parse(JSON.parse(req.body.data)) 29 | return SpecialtiesController.inserIntoDB(req, res, next) 30 | } 31 | ); 32 | 33 | 34 | 35 | // Task 2: Delete Specialties Data by ID 36 | 37 | /** 38 | - Develop an API endpoint to delete specialties by ID. 39 | - Implement an HTTP DELETE endpoint accepting the specialty ID. 40 | - Delete the specialty from the database and return a success message. 41 | - ENDPOINT: /specialties/:id 42 | */ 43 | 44 | router.delete( 45 | '/:id', 46 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 47 | SpecialtiesController.deleteFromDB 48 | ); 49 | 50 | export const SpecialtiesRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/Specialties/specialties.service.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import { fileUploader } from "../../../helpars/fileUploader"; 3 | import prisma from "../../../shared/prisma"; 4 | import { IFile } from "../../interfaces/file"; 5 | import { Specialties } from "@prisma/client"; 6 | 7 | const inserIntoDB = async (req: Request) => { 8 | 9 | const file = req.file as IFile; 10 | 11 | if (file) { 12 | const uploadToCloudinary = await fileUploader.uploadToCloudinary(file); 13 | req.body.icon = uploadToCloudinary?.secure_url; 14 | } 15 | 16 | const result = await prisma.specialties.create({ 17 | data: req.body 18 | }); 19 | 20 | return result; 21 | }; 22 | 23 | const getAllFromDB = async (): Promise => { 24 | return await prisma.specialties.findMany(); 25 | } 26 | 27 | const deleteFromDB = async (id: string): Promise => { 28 | const result = await prisma.specialties.delete({ 29 | where: { 30 | id, 31 | }, 32 | }); 33 | return result; 34 | }; 35 | 36 | export const SpecialtiesService = { 37 | inserIntoDB, 38 | getAllFromDB, 39 | deleteFromDB 40 | } -------------------------------------------------------------------------------- /src/app/modules/Specialties/specialties.validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const create = z.object({ 4 | title: z.string({ 5 | required_error: "Title is required!" 6 | }) 7 | }); 8 | 9 | export const SpecialtiesValidtaion = { 10 | create 11 | } -------------------------------------------------------------------------------- /src/app/modules/User/user.constant.ts: -------------------------------------------------------------------------------- 1 | export const userSearchAbleFields: string[] = ['email']; // only for search term 2 | 3 | export const userFilterableFields: string[] = [ 4 | 'email', 5 | 'role', 6 | 'status', 7 | 'searchTerm' 8 | ]; // for all filtering -------------------------------------------------------------------------------- /src/app/modules/User/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestHandler, Response } from "express"; 2 | import { userService } from "./user.sevice"; 3 | import catchAsync from "../../../shared/catchAsync"; 4 | import sendResponse from "../../../shared/sendResponse"; 5 | import httpStatus from "http-status"; 6 | import pick from "../../../shared/pick"; 7 | import { userFilterableFields } from "./user.constant"; 8 | import { UserRole } from "@prisma/client"; 9 | import { JwtPayload } from "jsonwebtoken"; 10 | import { IAuthUser } from "../../interfaces/common"; 11 | 12 | const createAdmin = catchAsync(async (req: Request, res: Response) => { 13 | 14 | const result = await userService.createAdmin(req); 15 | sendResponse(res, { 16 | statusCode: httpStatus.OK, 17 | success: true, 18 | message: "Admin Created successfuly!", 19 | data: result 20 | }) 21 | }); 22 | 23 | const createDoctor = catchAsync(async (req: Request, res: Response) => { 24 | 25 | const result = await userService.createDoctor(req); 26 | sendResponse(res, { 27 | statusCode: httpStatus.OK, 28 | success: true, 29 | message: "Doctor Created successfuly!", 30 | data: result 31 | }) 32 | }); 33 | 34 | const createPatient = catchAsync(async (req: Request, res: Response) => { 35 | 36 | const result = await userService.createPatient(req); 37 | sendResponse(res, { 38 | statusCode: httpStatus.OK, 39 | success: true, 40 | message: "Patient Created successfuly!", 41 | data: result 42 | }) 43 | }); 44 | 45 | const getAllFromDB = catchAsync(async (req: Request, res: Response) => { 46 | // console.log(req.query) 47 | const filters = pick(req.query, userFilterableFields); 48 | const options = pick(req.query, ['limit', 'page', 'sortBy', 'sortOrder']) 49 | 50 | const result = await userService.getAllFromDB(filters, options) 51 | 52 | sendResponse(res, { 53 | statusCode: httpStatus.OK, 54 | success: true, 55 | message: "Users data fetched!", 56 | meta: result.meta, 57 | data: result.data 58 | }) 59 | }); 60 | 61 | const changeProfileStatus = catchAsync(async (req: Request, res: Response) => { 62 | 63 | const { id } = req.params; 64 | const result = await userService.changeProfileStatus(id, req.body) 65 | 66 | sendResponse(res, { 67 | statusCode: httpStatus.OK, 68 | success: true, 69 | message: "Users profile status changed!", 70 | data: result 71 | }) 72 | }); 73 | 74 | 75 | const getMyProfile = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 76 | 77 | const user = req.user; 78 | 79 | const result = await userService.getMyProfile(user as IAuthUser); 80 | 81 | sendResponse(res, { 82 | statusCode: httpStatus.OK, 83 | success: true, 84 | message: "My profile data fetched!", 85 | data: result 86 | }) 87 | }); 88 | 89 | const updateMyProfie = catchAsync(async (req: Request & { user?: IAuthUser }, res: Response) => { 90 | 91 | const user = req.user; 92 | 93 | const result = await userService.updateMyProfie(user as IAuthUser, req); 94 | 95 | sendResponse(res, { 96 | statusCode: httpStatus.OK, 97 | success: true, 98 | message: "My profile updated!", 99 | data: result 100 | }) 101 | }); 102 | 103 | export const userController = { 104 | createAdmin, 105 | createDoctor, 106 | createPatient, 107 | getAllFromDB, 108 | changeProfileStatus, 109 | getMyProfile, 110 | updateMyProfie 111 | } -------------------------------------------------------------------------------- /src/app/modules/User/user.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express'; 2 | import { userController } from './user.controller'; 3 | import auth from '../../middlewares/auth'; 4 | import { UserRole } from '@prisma/client'; 5 | import { fileUploader } from '../../../helpars/fileUploader'; 6 | import { userValidation } from './user.validation'; 7 | import validateRequest from '../../middlewares/validateRequest'; 8 | 9 | const router = express.Router(); 10 | 11 | router.get( 12 | '/', 13 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 14 | userController.getAllFromDB 15 | ); 16 | 17 | router.get( 18 | '/me', 19 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT), 20 | userController.getMyProfile 21 | ) 22 | 23 | router.post( 24 | "/create-admin", 25 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 26 | fileUploader.upload.single('file'), 27 | (req: Request, res: Response, next: NextFunction) => { 28 | req.body = userValidation.createAdmin.parse(JSON.parse(req.body.data)) 29 | return userController.createAdmin(req, res, next) 30 | } 31 | ); 32 | 33 | router.post( 34 | "/create-doctor", 35 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 36 | fileUploader.upload.single('file'), 37 | (req: Request, res: Response, next: NextFunction) => { 38 | req.body = userValidation.createDoctor.parse(JSON.parse(req.body.data)) 39 | return userController.createDoctor(req, res, next) 40 | } 41 | ); 42 | 43 | router.post( 44 | "/create-patient", 45 | fileUploader.upload.single('file'), 46 | (req: Request, res: Response, next: NextFunction) => { 47 | req.body = userValidation.createPatient.parse(JSON.parse(req.body.data)) 48 | return userController.createPatient(req, res, next) 49 | } 50 | ); 51 | 52 | router.patch( 53 | '/:id/status', 54 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN), 55 | validateRequest(userValidation.updateStatus), 56 | userController.changeProfileStatus 57 | ); 58 | 59 | router.patch( 60 | "/update-my-profile", 61 | auth(UserRole.SUPER_ADMIN, UserRole.ADMIN, UserRole.DOCTOR, UserRole.PATIENT), 62 | fileUploader.upload.single('file'), 63 | (req: Request, res: Response, next: NextFunction) => { 64 | req.body = JSON.parse(req.body.data) 65 | return userController.updateMyProfie(req, res, next) 66 | } 67 | ); 68 | 69 | 70 | export const userRoutes = router; -------------------------------------------------------------------------------- /src/app/modules/User/user.sevice.ts: -------------------------------------------------------------------------------- 1 | import { Admin, Doctor, Patient, Prisma, User, UserRole, UserStatus } from "@prisma/client"; 2 | import * as bcrypt from 'bcrypt' 3 | import prisma from "../../../shared/prisma"; 4 | import { fileUploader } from "../../../helpars/fileUploader"; 5 | import { IFile } from "../../interfaces/file"; 6 | import { Request } from "express"; 7 | import { IPaginationOptions } from "../../interfaces/pagination"; 8 | import { paginationHelper } from "../../../helpars/paginationHelper"; 9 | import { userSearchAbleFields } from "./user.constant"; 10 | import { IAuthUser } from "../../interfaces/common"; 11 | 12 | const createAdmin = async (req: Request): Promise => { 13 | 14 | const file = req.file as IFile; 15 | 16 | if (file) { 17 | const uploadToCloudinary = await fileUploader.uploadToCloudinary(file); 18 | req.body.admin.profilePhoto = uploadToCloudinary?.secure_url 19 | } 20 | 21 | const hashedPassword: string = await bcrypt.hash(req.body.password, 12) 22 | 23 | const userData = { 24 | email: req.body.admin.email, 25 | password: hashedPassword, 26 | role: UserRole.ADMIN 27 | } 28 | 29 | const result = await prisma.$transaction(async (transactionClient) => { 30 | await transactionClient.user.create({ 31 | data: userData 32 | }); 33 | 34 | const createdAdminData = await transactionClient.admin.create({ 35 | data: req.body.admin 36 | }); 37 | 38 | return createdAdminData; 39 | }); 40 | 41 | return result; 42 | }; 43 | 44 | const createDoctor = async (req: Request): Promise => { 45 | 46 | const file = req.file as IFile; 47 | 48 | if (file) { 49 | const uploadToCloudinary = await fileUploader.uploadToCloudinary(file); 50 | req.body.doctor.profilePhoto = uploadToCloudinary?.secure_url 51 | } 52 | 53 | const hashedPassword: string = await bcrypt.hash(req.body.password, 12) 54 | 55 | const userData = { 56 | email: req.body.doctor.email, 57 | password: hashedPassword, 58 | role: UserRole.DOCTOR 59 | } 60 | 61 | const result = await prisma.$transaction(async (transactionClient) => { 62 | await transactionClient.user.create({ 63 | data: userData 64 | }); 65 | 66 | const createdDoctorData = await transactionClient.doctor.create({ 67 | data: req.body.doctor 68 | }); 69 | 70 | return createdDoctorData; 71 | }); 72 | 73 | return result; 74 | }; 75 | 76 | const createPatient = async (req: Request): Promise => { 77 | const file = req.file as IFile; 78 | 79 | if (file) { 80 | const uploadedProfileImage = await fileUploader.uploadToCloudinary(file); 81 | req.body.patient.profilePhoto = uploadedProfileImage?.secure_url; 82 | } 83 | 84 | const hashedPassword: string = await bcrypt.hash(req.body.password, 12) 85 | 86 | const userData = { 87 | email: req.body.patient.email, 88 | password: hashedPassword, 89 | role: UserRole.PATIENT 90 | } 91 | 92 | const result = await prisma.$transaction(async (transactionClient) => { 93 | await transactionClient.user.create({ 94 | data: userData 95 | }); 96 | 97 | const createdPatientData = await transactionClient.patient.create({ 98 | data: req.body.patient 99 | }); 100 | 101 | return createdPatientData; 102 | }); 103 | 104 | return result; 105 | }; 106 | 107 | const getAllFromDB = async (params: any, options: IPaginationOptions) => { 108 | const { page, limit, skip } = paginationHelper.calculatePagination(options); 109 | const { searchTerm, ...filterData } = params; 110 | 111 | const andCondions: Prisma.UserWhereInput[] = []; 112 | 113 | //console.log(filterData); 114 | if (params.searchTerm) { 115 | andCondions.push({ 116 | OR: userSearchAbleFields.map(field => ({ 117 | [field]: { 118 | contains: params.searchTerm, 119 | mode: 'insensitive' 120 | } 121 | })) 122 | }) 123 | }; 124 | 125 | if (Object.keys(filterData).length > 0) { 126 | andCondions.push({ 127 | AND: Object.keys(filterData).map(key => ({ 128 | [key]: { 129 | equals: (filterData as any)[key] 130 | } 131 | })) 132 | }) 133 | }; 134 | 135 | const whereConditons: Prisma.UserWhereInput = andCondions.length > 0 ? { AND: andCondions } : {}; 136 | 137 | const result = await prisma.user.findMany({ 138 | where: whereConditons, 139 | skip, 140 | take: limit, 141 | orderBy: options.sortBy && options.sortOrder ? { 142 | [options.sortBy]: options.sortOrder 143 | } : { 144 | createdAt: 'desc' 145 | }, 146 | select: { 147 | id: true, 148 | email: true, 149 | role: true, 150 | needPasswordChange: true, 151 | status: true, 152 | createdAt: true, 153 | updatedAt: true, 154 | admin: true, 155 | patient: true, 156 | doctor: true 157 | } 158 | }); 159 | 160 | const total = await prisma.user.count({ 161 | where: whereConditons 162 | }); 163 | 164 | return { 165 | meta: { 166 | page, 167 | limit, 168 | total 169 | }, 170 | data: result 171 | }; 172 | }; 173 | 174 | const changeProfileStatus = async (id: string, status: UserRole) => { 175 | const userData = await prisma.user.findUniqueOrThrow({ 176 | where: { 177 | id 178 | } 179 | }); 180 | 181 | const updateUserStatus = await prisma.user.update({ 182 | where: { 183 | id 184 | }, 185 | data: status 186 | }); 187 | 188 | return updateUserStatus; 189 | }; 190 | 191 | const getMyProfile = async (user: IAuthUser) => { 192 | 193 | const userInfo = await prisma.user.findUniqueOrThrow({ 194 | where: { 195 | email: user?.email, 196 | status: UserStatus.ACTIVE 197 | }, 198 | select: { 199 | id: true, 200 | email: true, 201 | needPasswordChange: true, 202 | role: true, 203 | status: true 204 | } 205 | }); 206 | 207 | let profileInfo; 208 | 209 | if (userInfo.role === UserRole.SUPER_ADMIN) { 210 | profileInfo = await prisma.admin.findUnique({ 211 | where: { 212 | email: userInfo.email 213 | } 214 | }) 215 | } 216 | else if (userInfo.role === UserRole.ADMIN) { 217 | profileInfo = await prisma.admin.findUnique({ 218 | where: { 219 | email: userInfo.email 220 | } 221 | }) 222 | } 223 | else if (userInfo.role === UserRole.DOCTOR) { 224 | profileInfo = await prisma.doctor.findUnique({ 225 | where: { 226 | email: userInfo.email 227 | } 228 | }) 229 | } 230 | else if (userInfo.role === UserRole.PATIENT) { 231 | profileInfo = await prisma.patient.findUnique({ 232 | where: { 233 | email: userInfo.email 234 | } 235 | }) 236 | } 237 | 238 | return { ...userInfo, ...profileInfo }; 239 | }; 240 | 241 | 242 | const updateMyProfie = async (user: IAuthUser, req: Request) => { 243 | const userInfo = await prisma.user.findUniqueOrThrow({ 244 | where: { 245 | email: user?.email, 246 | status: UserStatus.ACTIVE 247 | } 248 | }); 249 | 250 | const file = req.file as IFile; 251 | if (file) { 252 | const uploadToCloudinary = await fileUploader.uploadToCloudinary(file); 253 | req.body.profilePhoto = uploadToCloudinary?.secure_url; 254 | } 255 | 256 | let profileInfo; 257 | 258 | if (userInfo.role === UserRole.SUPER_ADMIN) { 259 | profileInfo = await prisma.admin.update({ 260 | where: { 261 | email: userInfo.email 262 | }, 263 | data: req.body 264 | }) 265 | } 266 | else if (userInfo.role === UserRole.ADMIN) { 267 | profileInfo = await prisma.admin.update({ 268 | where: { 269 | email: userInfo.email 270 | }, 271 | data: req.body 272 | }) 273 | } 274 | else if (userInfo.role === UserRole.DOCTOR) { 275 | profileInfo = await prisma.doctor.update({ 276 | where: { 277 | email: userInfo.email 278 | }, 279 | data: req.body 280 | }) 281 | } 282 | else if (userInfo.role === UserRole.PATIENT) { 283 | profileInfo = await prisma.patient.update({ 284 | where: { 285 | email: userInfo.email 286 | }, 287 | data: req.body 288 | }) 289 | } 290 | 291 | return { ...profileInfo }; 292 | } 293 | 294 | 295 | export const userService = { 296 | createAdmin, 297 | createDoctor, 298 | createPatient, 299 | getAllFromDB, 300 | changeProfileStatus, 301 | getMyProfile, 302 | updateMyProfie 303 | } -------------------------------------------------------------------------------- /src/app/modules/User/user.validation.ts: -------------------------------------------------------------------------------- 1 | import { Gender, UserRole, UserStatus } from "@prisma/client"; 2 | import { z } from "zod"; 3 | 4 | const createAdmin = z.object({ 5 | password: z.string({ 6 | required_error: "Password is required" 7 | }), 8 | admin: z.object({ 9 | name: z.string({ 10 | required_error: "Name is required!" 11 | }), 12 | email: z.string({ 13 | required_error: "Email is required!" 14 | }), 15 | contactNumber: z.string({ 16 | required_error: "Contact Number is required!" 17 | }) 18 | }) 19 | }); 20 | 21 | const createDoctor = z.object({ 22 | password: z.string({ 23 | required_error: "Password is required" 24 | }), 25 | doctor: z.object({ 26 | name: z.string({ 27 | required_error: "Name is required!" 28 | }), 29 | email: z.string({ 30 | required_error: "Email is required!" 31 | }), 32 | contactNumber: z.string({ 33 | required_error: "Contact Number is required!" 34 | }), 35 | address: z.string().optional(), 36 | registrationNumber: z.string({ 37 | required_error: "Reg number is required" 38 | }), 39 | experience: z.number().optional(), 40 | gender: z.enum([Gender.MALE, Gender.FEMALE]), 41 | appointmentFee: z.number({ 42 | required_error: "appointment fee is required" 43 | }), 44 | qualification: z.string({ 45 | required_error: "quilification is required" 46 | }), 47 | currentWorkingPlace: z.string({ 48 | required_error: "Current working place is required!" 49 | }), 50 | designation: z.string({ 51 | required_error: "Designation is required!" 52 | }) 53 | }) 54 | }); 55 | 56 | const createPatient = z.object({ 57 | password: z.string(), 58 | patient: z.object({ 59 | email: z.string({ 60 | required_error: "Email is required!" 61 | }).email(), 62 | name: z.string({ 63 | required_error: "Name is required!" 64 | }), 65 | contactNumber: z.string({ 66 | required_error: "Contact number is required!" 67 | }), 68 | address: z.string({ 69 | required_error: "Address is required" 70 | }) 71 | }) 72 | }); 73 | 74 | const updateStatus = z.object({ 75 | body: z.object({ 76 | status: z.enum([UserStatus.ACTIVE, UserStatus.BLOCKED, UserStatus.DELETED]) 77 | }) 78 | }) 79 | 80 | export const userValidation = { 81 | createAdmin, 82 | createDoctor, 83 | createPatient, 84 | updateStatus 85 | } -------------------------------------------------------------------------------- /src/app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { userRoutes } from '../modules/User/user.routes'; 3 | import { AdminRoutes } from '../modules/Admin/admin.routes'; 4 | import { AuthRoutes } from '../modules/Auth/auth.routes'; 5 | import { SpecialtiesRoutes } from '../modules/Specialties/specialties.routes'; 6 | import { DoctorRoutes } from '../modules/Doctor/doctor.routes'; 7 | import { PatientRoutes } from '../modules/Patient/patient.route'; 8 | import { ScheduleRoutes } from '../modules/Schedule/schedule.routes'; 9 | import { DoctorScheduleRoutes } from '../modules/DoctorSchedule/doctorSchedule.routes'; 10 | import { AppointmentRoutes } from '../modules/Appointment/appointment.routes'; 11 | import { PaymentRoutes } from '../modules/Payment/payment.routes'; 12 | import { PrescriptionRoutes } from '../modules/Prescription/prescription.routes'; 13 | import { ReviewRoutes } from '../modules/Review/review.routes'; 14 | import { MetaRoutes } from '../modules/Meta/meta.routes'; 15 | 16 | const router = express.Router(); 17 | 18 | const moduleRoutes = [ 19 | { 20 | path: '/user', 21 | route: userRoutes 22 | }, 23 | { 24 | path: '/admin', 25 | route: AdminRoutes 26 | }, 27 | { 28 | path: '/auth', 29 | route: AuthRoutes 30 | }, 31 | { 32 | path: '/specialties', 33 | route: SpecialtiesRoutes 34 | }, 35 | { 36 | path: '/doctor', 37 | route: DoctorRoutes 38 | }, 39 | { 40 | path: '/patient', 41 | route: PatientRoutes 42 | }, 43 | { 44 | path: '/schedule', 45 | route: ScheduleRoutes 46 | }, 47 | { 48 | path: '/doctor-schedule', 49 | route: DoctorScheduleRoutes 50 | }, 51 | { 52 | path: '/appointment', 53 | route: AppointmentRoutes 54 | }, 55 | { 56 | path: '/payment', 57 | route: PaymentRoutes 58 | }, 59 | { 60 | path: '/prescription', 61 | route: PrescriptionRoutes 62 | }, 63 | { 64 | path: '/review', 65 | route: ReviewRoutes 66 | }, 67 | { 68 | path: '/meta', 69 | route: MetaRoutes 70 | } 71 | ]; 72 | 73 | moduleRoutes.forEach(route => router.use(route.path, route.route)) 74 | 75 | export default router; -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | 4 | dotenv.config({ path: path.join(process.cwd(), '.env') }); 5 | 6 | export default { 7 | env: process.env.NODE_ENV, 8 | port: process.env.PORT, 9 | jwt: { 10 | jwt_secret: process.env.JWT_SECRET, 11 | expires_in: process.env.EXPIRES_IN, 12 | refresh_token_secret: process.env.REFRESH_TOKEN_SECRET, 13 | refresh_token_expires_in: process.env.REFRESH_TOKEN_EXPIRES_IN, 14 | reset_pass_secret: process.env.RESET_PASS_TOKEN, 15 | reset_pass_token_expires_in: process.env.RESET_PASS_TOKEN_EXPIRES_IN 16 | }, 17 | reset_pass_link: process.env.RESET_PASS_LINK, 18 | emailSender: { 19 | email: process.env.EMAIL, 20 | app_pass: process.env.APP_PASS 21 | }, 22 | ssl: { 23 | storeId: process.env.STORE_ID, 24 | storePass: process.env.STORE_PASS, 25 | successUrl: process.env.SUCCESS_URL, 26 | cancelUrl: process.env.CANCEL_URL, 27 | failUrl: process.env.FAIL_URL, 28 | sslPaymentApi: process.env.SSL_PAYMENT_API, 29 | sslValidationApi: process.env.SSL_VALIDATIOIN_API 30 | } 31 | } -------------------------------------------------------------------------------- /src/helpars/fileUploader.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer" 2 | import path from "path" 3 | import fs from 'fs' 4 | import { v2 as cloudinary } from 'cloudinary'; 5 | import { ICloudinaryResponse, IFile } from "../app/interfaces/file"; 6 | 7 | 8 | cloudinary.config({ 9 | cloud_name: 'dbgrq28js', 10 | api_key: '173484379744282', 11 | api_secret: 'eHKsVTxIOLl5oaO_BHxBQWAK3GA' 12 | }); 13 | 14 | const storage = multer.diskStorage({ 15 | destination: function (req, file, cb) { 16 | cb(null, path.join(process.cwd(), 'uploads')) 17 | }, 18 | filename: function (req, file, cb) { 19 | cb(null, file.originalname) 20 | } 21 | }) 22 | 23 | const upload = multer({ storage: storage }) 24 | 25 | const uploadToCloudinary = async (file: IFile): Promise => { 26 | return new Promise((resolve, reject) => { 27 | cloudinary.uploader.upload(file.path, 28 | (error: Error, result: ICloudinaryResponse) => { 29 | fs.unlinkSync(file.path) 30 | if (error) { 31 | reject(error) 32 | } 33 | else { 34 | resolve(result) 35 | } 36 | }) 37 | }) 38 | }; 39 | 40 | export const fileUploader = { 41 | upload, 42 | uploadToCloudinary 43 | } -------------------------------------------------------------------------------- /src/helpars/jwtHelpers.ts: -------------------------------------------------------------------------------- 1 | import jwt, { JwtPayload, Secret } from 'jsonwebtoken'; 2 | 3 | const generateToken = (payload: any, secret: Secret, expiresIn: string) => { 4 | const token = jwt.sign( 5 | payload, 6 | secret, 7 | { 8 | algorithm: 'HS256', 9 | expiresIn 10 | } 11 | ); 12 | 13 | return token; 14 | }; 15 | 16 | const verifyToken = (token: string, secret: Secret) => { 17 | return jwt.verify(token, secret) as JwtPayload; 18 | } 19 | 20 | export const jwtHelpers = { 21 | generateToken, 22 | verifyToken 23 | } -------------------------------------------------------------------------------- /src/helpars/paginationHelper.ts: -------------------------------------------------------------------------------- 1 | type IOptions = { 2 | page?: number, 3 | limit?: number, 4 | sortOrder?: string, 5 | sortBy?: string 6 | } 7 | 8 | type IOptionsResult = { 9 | page: number, 10 | limit: number, 11 | skip: number, 12 | sortBy: string, 13 | sortOrder: string 14 | } 15 | 16 | const calculatePagination = (options: IOptions): IOptionsResult => { 17 | 18 | const page: number = Number(options.page) || 1; 19 | const limit: number = Number(options.limit) || 10; 20 | const skip: number = (Number(page) - 1) * limit; 21 | 22 | const sortBy: string = options.sortBy || 'createdAt'; 23 | const sortOrder: string = options.sortOrder || 'desc'; 24 | 25 | return { 26 | page, 27 | limit, 28 | skip, 29 | sortBy, 30 | sortOrder 31 | } 32 | } 33 | 34 | 35 | export const paginationHelper = { 36 | calculatePagination 37 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import app from './app' 3 | import config from './config'; 4 | 5 | 6 | 7 | async function main() { 8 | const server: Server = app.listen(config.port, () => { 9 | console.log("Sever is running on port ", config.port); 10 | }); 11 | 12 | const exitHandler = () => { 13 | if (server) { 14 | server.close(() => { 15 | console.info("Server closed!") 16 | }) 17 | } 18 | process.exit(1); 19 | }; 20 | process.on('uncaughtException', (error) => { 21 | console.log(error); 22 | exitHandler(); 23 | }); 24 | 25 | process.on('unhandledRejection', (error) => { 26 | console.log(error); 27 | exitHandler(); 28 | }) 29 | }; 30 | 31 | main(); -------------------------------------------------------------------------------- /src/shared/catchAsync.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from "express"; 2 | 3 | const catchAsync = (fn: RequestHandler) => { 4 | return async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | await fn(req, res, next) 7 | } 8 | catch (err) { 9 | next(err); 10 | } 11 | } 12 | }; 13 | 14 | export default catchAsync; -------------------------------------------------------------------------------- /src/shared/pick.ts: -------------------------------------------------------------------------------- 1 | const pick = , k extends keyof T>(obj: T, keys: k[]): Partial => { 2 | const finalObj: Partial = {}; 3 | 4 | for (const key of keys) { 5 | if (obj && Object.hasOwnProperty.call(obj, key)) { 6 | finalObj[key] = obj[key] 7 | } 8 | } 9 | 10 | console.log(finalObj) 11 | return finalObj; 12 | } 13 | 14 | export default pick; -------------------------------------------------------------------------------- /src/shared/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prisma = new PrismaClient({ 4 | log: [ 5 | { 6 | emit: 'event', 7 | level: 'query', 8 | }, 9 | { 10 | emit: 'event', 11 | level: 'error', 12 | }, 13 | { 14 | emit: 'event', 15 | level: 'info', 16 | }, 17 | { 18 | emit: 'event', 19 | level: 'warn', 20 | }, 21 | ], 22 | }) 23 | 24 | prisma.$on('query', (e) => { 25 | console.log("-------------------------------------------") 26 | console.log('Query: ' + e.query); 27 | console.log("-------------------------------------------") 28 | console.log('Params: ' + e.params) 29 | console.log("-------------------------------------------") 30 | console.log('Duration: ' + e.duration + 'ms') 31 | console.log("-------------------------------------------") 32 | }) 33 | 34 | // prisma.$on('warn', (e) => { 35 | // console.log(e) 36 | // }) 37 | 38 | // prisma.$on('info', (e) => { 39 | // console.log(e) 40 | // }) 41 | 42 | // prisma.$on('error', (e) => { 43 | // console.log(e) 44 | // }) 45 | 46 | export default prisma; -------------------------------------------------------------------------------- /src/shared/sendResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express" 2 | 3 | const sendResponse = (res: Response, jsonData: { 4 | statusCode: number, 5 | success: boolean, 6 | message: string, 7 | meta?: { 8 | page: number, 9 | limit: number, 10 | total: number 11 | }, 12 | data: T | null | undefined 13 | }) => { 14 | res.status(jsonData.statusCode).json({ 15 | success: jsonData.success, 16 | message: jsonData.message, 17 | meta: jsonData.meta || null || undefined, 18 | data: jsonData.data || null || undefined 19 | }) 20 | } 21 | 22 | export default sendResponse; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "commonjs", /* Specify what module code is generated. */ 26 | "rootDir": "./src", /* Specify the root folder within your source files. */ 27 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | /* Type Checking */ 78 | "strict": true, /* Enable all strict type-checking options. */ 79 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 80 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 82 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 84 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 85 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 87 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 92 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } --------------------------------------------------------------------------------