first commit

This commit is contained in:
2026-03-26 08:14:56 +03:30
commit 3561c09e2d
128 changed files with 16084 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
# Keep environment variables out of version control
.env
/generated/prisma
/src/generated/prisma

20
mongodb/config/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import mongoose from "mongoose";
export async function connectMongo() {
if (mongoose.connection.readyState === 1) return;
await mongoose.connect(process.env.MONGO_URI!, {
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000,
});
console.log("MongoDB connected");
mongoose.connection.on("error", (err) => {
console.error("MongoDB connection error:", err);
});
mongoose.connection.on("disconnected", () => {
console.warn("MongoDB disconnected");
});
}

53
mongodb/models/index.ts Normal file
View File

@@ -0,0 +1,53 @@
import mongoose, {Schema} from "mongoose";
// API Request Logs
const ApiRequestLogSchema = new Schema({
method: {type: String, required: true},
path: {type: String, required: true},
status: {type: Number, required: true},
durationMs: {type: Number, required: true},
userId: {type: String, required: false},
role: {type: String},
createdAt: {type: Date, default: Date.now},
});
export const ApiRequestLog = mongoose.model(
"ApiRequestLog",
ApiRequestLogSchema
);
// Error Logs
const ErrorLogSchema = new Schema({
message: {type: String, required: true},
stack: {type: String},
severity: {type: String, default: "error"},
createdAt: {type: Date, default: Date.now},
});
export const ErrorLog = mongoose.model("ErrorLog", ErrorLogSchema);
// Security Event Logs
const SecurityEventSchema = new Schema({
type: {type: String, required: true},
userId: {type: String},
ip: {type: String},
createdAt: {type: Date, default: Date.now},
});
export const SecurityEventLog = mongoose.model(
"SecurityEventLog",
SecurityEventSchema
);
// Performance Logs
const PerformanceLogSchema = new Schema({
metric: {type: String, required: true},
valueMs: {type: Number, required: true},
endpoint: {type: String},
createdAt: {type: Date, default: Date.now},
});
export const PerformanceLog = mongoose.model(
"PerformanceLog",
PerformanceLogSchema
);

5
nodemon.json Normal file
View File

@@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "ts",
"exec": "ts-node-esm src/index.ts"
}

5661
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --watch src --exec ts-node -r tsconfig-paths/register src/index.ts",
"dev:ts": "nodemon",
"build": "tsc",
"start": "node dist/index.js"
},
"prisma": {
"schema": "./prisma"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/busboy": "^1.5.4",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/csurf": "^1.11.5",
"@types/express": "^5.0.5",
"@types/hpp": "^0.2.7",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"@types/pg": "^8.15.6",
"nodemon": "^3.1.11",
"prisma": "^7.0.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"auto-bind": "^4.0.0",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"busboy": "^1.6.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"dotenv": "^17.2.3",
"exceljs": "^4.4.0",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"file-type": "^21.1.1",
"form-data": "^4.0.5",
"helmet": "^8.1.0",
"hpp": "^0.2.3",
"http-errors": "^2.0.1",
"joi": "^18.0.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^9.0.2",
"multer": "^2.0.2",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.11",
"slugify": "^1.6.6",
"transliteration": "^2.3.5"
}
}

13
prisma.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
})

View File

@@ -0,0 +1,225 @@
-- CreateEnum
CREATE TYPE "UserRoles" AS ENUM ('DOCTOR', 'TEAM');
-- CreateEnum
CREATE TYPE "PatientStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED');
-- CreateEnum
CREATE TYPE "PageType" AS ENUM ('STATIC', 'SERVICE', 'PACKAGE', 'FORM_PAGE', 'CONTACT');
-- CreateEnum
CREATE TYPE "PageStatus" AS ENUM ('DRAFT', 'PUBLISHED');
-- CreateEnum
CREATE TYPE "BlockType" AS ENUM ('TEXT', 'TEAM', 'CONTACT_INFO', 'MAP', 'PACKAGE_LIST', 'ACCORDION', 'FORM', 'GALLERY');
-- CreateEnum
CREATE TYPE "UploadedBy" AS ENUM ('PATIENT', 'STAFF');
-- CreateEnum
CREATE TYPE "FormStatus" AS ENUM ('PENDING', 'REVIEWED', 'REJECTED', 'APPROVED');
-- CreateTable
CREATE TABLE "Users" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT NOT NULL,
"bio" TEXT,
"position" TEXT,
"expertise" TEXT,
"phone_number" TEXT,
"email" TEXT,
"image" TEXT,
"socials" JSONB,
"profile_photo_id" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Patient" (
"id" SERIAL NOT NULL,
"first_name" TEXT NOT NULL,
"last_name" TEXT NOT NULL,
"dob" TIMESTAMP(3) NOT NULL,
"email" TEXT,
"phone_number" TEXT,
"country_name" TEXT,
"country_code" TEXT,
"status" "PatientStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Patient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Service" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"intro" JSONB,
"image" TEXT,
"description" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Page" (
"id" SERIAL NOT NULL,
"type" "PageType" NOT NULL,
"featured_image" TEXT,
"status" "PageStatus" NOT NULL DEFAULT 'PUBLISHED',
CONSTRAINT "Page_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PageBlock" (
"id" SERIAL NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"blockType" "BlockType" NOT NULL,
"data" JSONB NOT NULL,
"pageId" INTEGER NOT NULL,
CONSTRAINT "PageBlock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Package" (
"id" SERIAL NOT NULL,
"service_id" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"price" DOUBLE PRECISION,
"image" TEXT,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Package_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PackageItem" (
"id" SERIAL NOT NULL,
"package_id" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"content" JSONB,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PackageItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MedicalDocument" (
"id" SERIAL NOT NULL,
"patient_id" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"uploadedBy" "UploadedBy" NOT NULL DEFAULT 'PATIENT',
"uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MedicalDocument_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Language" (
"id" SERIAL NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Language_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Image" (
"id" SERIAL NOT NULL,
"code" TEXT,
"url" TEXT NOT NULL,
"name" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL,
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "File" (
"id" SERIAL NOT NULL,
"code" TEXT,
"url" TEXT NOT NULL,
"name" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"uploadedBy" "UploadedBy" NOT NULL DEFAULT 'PATIENT',
"uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"documentId" INTEGER,
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DefaultInfo" (
"id" SERIAL NOT NULL,
"address" TEXT,
"phone_number" TEXT,
"email" TEXT,
"mapEmbed" JSONB,
"ipd_technician_phone" TEXT NOT NULL,
"ipd_email" TEXT,
"instagram_link" TEXT,
"linkedin_link" TEXT,
"under_logo_description" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DefaultInfo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdmissionForm" (
"id" SERIAL NOT NULL,
"patient_id" INTEGER NOT NULL,
"data" JSONB NOT NULL,
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" "FormStatus" NOT NULL DEFAULT 'PENDING',
CONSTRAINT "AdmissionForm_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Patient_email_key" ON "Patient"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Service_slug_key" ON "Service"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Language_code_key" ON "Language"("code");
-- AddForeignKey
ALTER TABLE "Users" ADD CONSTRAINT "Users_profile_photo_id_fkey" FOREIGN KEY ("profile_photo_id") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PageBlock" ADD CONSTRAINT "PageBlock_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Package" ADD CONSTRAINT "Package_service_id_fkey" FOREIGN KEY ("service_id") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PackageItem" ADD CONSTRAINT "PackageItem_package_id_fkey" FOREIGN KEY ("package_id") REFERENCES "Package"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MedicalDocument" ADD CONSTRAINT "MedicalDocument_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "File" ADD CONSTRAINT "File_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "MedicalDocument"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AdmissionForm" ADD CONSTRAINT "AdmissionForm_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,284 @@
/*
Warnings:
- You are about to drop the column `code` on the `Image` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `Image` table. All the data in the column will be lost.
- You are about to drop the column `url` on the `Image` table. All the data in the column will be lost.
- The primary key for the `Patient` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `country_code` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `country_name` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `dob` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `first_name` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `last_name` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `phone_number` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `status` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `bio` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `image` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `phone_number` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `profile_photo_id` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `role` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `socials` on the `Users` table. All the data in the column will be lost.
- You are about to drop the `AdmissionForm` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `DefaultInfo` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `File` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Language` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `MedicalDocument` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Package` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `PackageItem` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Page` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `PageBlock` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Service` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `firstName` to the `Patient` table without a default value. This is not possible if the table is not empty.
- Added the required column `lastName` to the `Patient` table without a default value. This is not possible if the table is not empty.
- Added the required column `firstName` to the `Users` table without a default value. This is not possible if the table is not empty.
- Added the required column `lastName` to the `Users` table without a default value. This is not possible if the table is not empty.
- Made the column `position` on table `Users` required. This step will fail if there are existing NULL values in that column.
*/
-- CreateEnum
CREATE TYPE "CaseStatus" AS ENUM ('NEW', 'CONTACTED', 'DOCS_PENDING', 'REVIEWING', 'PRE_APPROVED', 'REJECTED', 'CLOSED', 'CONVERTED_TO_HIS');
-- CreateEnum
CREATE TYPE "DocumentType" AS ENUM ('PASSPORT', 'MEDICAL_RECORD', 'IMAGING', 'LAB_RESULT', 'OTHER');
-- CreateEnum
CREATE TYPE "InteractionType" AS ENUM ('PHONE', 'WHATSAPP', 'EMAIL', 'SYSTEM');
-- CreateEnum
CREATE TYPE "UsersType" AS ENUM ('DOCTOR', 'TRANSFER_TEAM', 'DEPARTMENT');
-- DropForeignKey
ALTER TABLE "AdmissionForm" DROP CONSTRAINT "AdmissionForm_patient_id_fkey";
-- DropForeignKey
ALTER TABLE "File" DROP CONSTRAINT "File_documentId_fkey";
-- DropForeignKey
ALTER TABLE "MedicalDocument" DROP CONSTRAINT "MedicalDocument_patient_id_fkey";
-- DropForeignKey
ALTER TABLE "Package" DROP CONSTRAINT "Package_service_id_fkey";
-- DropForeignKey
ALTER TABLE "PackageItem" DROP CONSTRAINT "PackageItem_package_id_fkey";
-- DropForeignKey
ALTER TABLE "PageBlock" DROP CONSTRAINT "PageBlock_pageId_fkey";
-- DropForeignKey
ALTER TABLE "Users" DROP CONSTRAINT "Users_profile_photo_id_fkey";
-- DropIndex
DROP INDEX "Patient_email_key";
-- AlterTable
ALTER TABLE "Image" DROP COLUMN "code",
DROP COLUMN "name",
DROP COLUMN "url",
ADD COLUMN "filename" TEXT,
ALTER COLUMN "mimeType" DROP NOT NULL,
ALTER COLUMN "size" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Patient" DROP CONSTRAINT "Patient_pkey",
DROP COLUMN "country_code",
DROP COLUMN "country_name",
DROP COLUMN "dob",
DROP COLUMN "first_name",
DROP COLUMN "last_name",
DROP COLUMN "phone_number",
DROP COLUMN "status",
ADD COLUMN "age" INTEGER,
ADD COLUMN "countryCode" TEXT,
ADD COLUMN "firstName" TEXT NOT NULL,
ADD COLUMN "lastName" TEXT NOT NULL,
ADD COLUMN "nationality" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "preferredLanguage" TEXT,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Patient_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Patient_id_seq";
-- AlterTable
ALTER TABLE "Users" DROP COLUMN "bio",
DROP COLUMN "createdAt",
DROP COLUMN "image",
DROP COLUMN "name",
DROP COLUMN "phone_number",
DROP COLUMN "profile_photo_id",
DROP COLUMN "role",
DROP COLUMN "socials",
ADD COLUMN "firstName" TEXT NOT NULL,
ADD COLUMN "imageId" INTEGER,
ADD COLUMN "lastName" TEXT NOT NULL,
ADD COLUMN "phone" TEXT,
ADD COLUMN "type" "UsersType" NOT NULL DEFAULT 'DOCTOR',
ALTER COLUMN "position" SET NOT NULL;
-- DropTable
DROP TABLE "AdmissionForm";
-- DropTable
DROP TABLE "DefaultInfo";
-- DropTable
DROP TABLE "File";
-- DropTable
DROP TABLE "Language";
-- DropTable
DROP TABLE "MedicalDocument";
-- DropTable
DROP TABLE "Package";
-- DropTable
DROP TABLE "PackageItem";
-- DropTable
DROP TABLE "Page";
-- DropTable
DROP TABLE "PageBlock";
-- DropTable
DROP TABLE "Service";
-- DropEnum
DROP TYPE "BlockType";
-- DropEnum
DROP TYPE "FormStatus";
-- DropEnum
DROP TYPE "PageStatus";
-- DropEnum
DROP TYPE "PageType";
-- DropEnum
DROP TYPE "PatientStatus";
-- DropEnum
DROP TYPE "UploadedBy";
-- DropEnum
DROP TYPE "UserRoles";
-- CreateTable
CREATE TABLE "OnlineCase" (
"id" TEXT NOT NULL,
"patientId" TEXT NOT NULL,
"trackingCode" TEXT NOT NULL,
"message" TEXT,
"specialty" TEXT,
"formData" JSONB,
"status" "CaseStatus" NOT NULL DEFAULT 'NEW',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OnlineCase_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Document" (
"id" TEXT NOT NULL,
"caseId" TEXT,
"patientId" TEXT,
"uploadedById" TEXT,
"type" "DocumentType" NOT NULL,
"fileKey" TEXT NOT NULL,
"filename" TEXT,
"mimeType" TEXT,
"size" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Interaction" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"staffId" TEXT,
"type" "InteractionType" NOT NULL,
"message" TEXT,
"direction" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Interaction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Review" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"doctorId" TEXT,
"note" TEXT,
"result" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseStatusHistory" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"from" "CaseStatus",
"to" "CaseStatus" NOT NULL,
"changedBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseStatusHistory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Staff" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"role" TEXT NOT NULL,
"email" TEXT NOT NULL,
CONSTRAINT "Staff_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "OnlineCase_trackingCode_key" ON "OnlineCase"("trackingCode");
-- CreateIndex
CREATE UNIQUE INDEX "Staff_email_key" ON "Staff"("email");
-- AddForeignKey
ALTER TABLE "OnlineCase" ADD CONSTRAINT "OnlineCase_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "Patient"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Interaction" ADD CONSTRAINT "Interaction_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Interaction" ADD CONSTRAINT "Interaction_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseStatusHistory" ADD CONSTRAINT "CaseStatusHistory_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Users" ADD CONSTRAINT "Users_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,17 @@
/*
Warnings:
- You are about to drop the column `email` on the `Staff` table. All the data in the column will be lost.
- A unique constraint covering the columns `[username]` on the table `Staff` will be added. If there are existing duplicate values, this will fail.
- Added the required column `password` to the `Staff` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "Staff_email_key";
-- AlterTable
ALTER TABLE "Staff" DROP COLUMN "email",
ADD COLUMN "password" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Staff_username_key" ON "Staff"("username");

View File

@@ -0,0 +1,711 @@
/*
Warnings:
- The values [MEDICAL_RECORD,IMAGING] on the enum `DocumentType` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `countryCode` on the `Patient` table. All the data in the column will be lost.
- You are about to drop the column `nationality` on the `Patient` table. All the data in the column will be lost.
- You are about to alter the column `email` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `firstName` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
- You are about to alter the column `lastName` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
- You are about to alter the column `phone` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`.
- You are about to alter the column `preferredLanguage` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(10)`.
- You are about to drop the column `note` on the `Review` table. All the data in the column will be lost.
- You are about to drop the column `result` on the `Review` table. All the data in the column will be lost.
- You are about to drop the column `expertise` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `firstName` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `lastName` on the `Users` table. All the data in the column will be lost.
- You are about to drop the column `position` on the `Users` table. All the data in the column will be lost.
- You are about to drop the `Interaction` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[pid]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[nationalityCode]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[passportCode,nationalityId]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[phone]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[email]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[firstName,lastName,birthDate]` on the table `Patient` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[email]` on the table `Staff` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[slug]` on the table `Users` will be added. If there are existing duplicate values, this will fail.
- Added the required column `fileUrl` to the `Document` table without a default value. This is not possible if the table is not empty.
- Made the column `filename` on table `Document` required. This step will fail if there are existing NULL values in that column.
- Added the required column `fileKey` to the `Image` table without a default value. This is not possible if the table is not empty.
- Added the required column `pid` to the `Patient` table without a default value. This is not possible if the table is not empty.
- Changed the type of `role` on the `Staff` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Added the required column `slug` to the `Users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "DocStatus" AS ENUM ('NEW', 'PENDING_SCAN', 'SAFE', 'INFECTED', 'INVALID');
-- CreateEnum
CREATE TYPE "Sex" AS ENUM ('male', 'female', 'other');
-- CreateEnum
CREATE TYPE "UploadStatus" AS ENUM ('PENDING', 'UPLOADING', 'UPLOADED', 'VERIFIED', 'FAILED', 'EXPIRED');
-- CreateEnum
CREATE TYPE "StaffRoles" AS ENUM ('developer', 'admin', 'doctor', 'coordinator');
-- CreateEnum
CREATE TYPE "StaffStatus" AS ENUM ('ACTIVE', 'WARNED', 'RESTRICTED', 'BANNED');
-- CreateEnum
CREATE TYPE "ViolationType" AS ENUM ('SPAM', 'RATE_LIMIT', 'CONTENT', 'ABUSE');
-- CreateEnum
CREATE TYPE "RestrictionType" AS ENUM ('TEMP', 'SHADOW', 'PERMANENT');
-- CreateEnum
CREATE TYPE "ConfigType" AS ENUM ('BOOLEAN', 'NUMBER', 'STRING', 'JSON', 'STRING_ARRAY');
-- CreateEnum
CREATE TYPE "AuditAction" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'READ');
-- AlterEnum
BEGIN;
CREATE TYPE "DocumentType_new" AS ENUM ('MEDICAL_IMAGE', 'LAB_RESULT', 'PRESCRIPTION', 'PASSPORT', 'NATIONAL_ID', 'OTHER', 'OTHER_FILE');
ALTER TABLE "Document" ALTER COLUMN "type" TYPE "DocumentType_new" USING ("type"::text::"DocumentType_new");
ALTER TYPE "DocumentType" RENAME TO "DocumentType_old";
ALTER TYPE "DocumentType_new" RENAME TO "DocumentType";
DROP TYPE "public"."DocumentType_old";
COMMIT;
-- DropForeignKey
ALTER TABLE "Interaction" DROP CONSTRAINT "Interaction_caseId_fkey";
-- DropForeignKey
ALTER TABLE "Interaction" DROP CONSTRAINT "Interaction_staffId_fkey";
-- AlterTable
ALTER TABLE "CaseStatusHistory" ALTER COLUMN "to" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "checksum" TEXT,
ADD COLUMN "fileUrl" TEXT NOT NULL,
ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "status" "DocStatus" NOT NULL DEFAULT 'NEW',
ALTER COLUMN "filename" SET NOT NULL;
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "fileKey" TEXT NOT NULL,
ADD COLUMN "fileUrl" TEXT;
-- AlterTable
ALTER TABLE "Patient" DROP COLUMN "countryCode",
DROP COLUMN "nationality",
ADD COLUMN "address" VARCHAR(500),
ADD COLUMN "birthDate" TIMESTAMP(3),
ADD COLUMN "deletedAt" TIMESTAMP(3),
ADD COLUMN "nationalityCode" CHAR(3),
ADD COLUMN "nationalityId" INTEGER,
ADD COLUMN "passportCode" VARCHAR(20),
ADD COLUMN "pid" VARCHAR(10) NOT NULL,
ADD COLUMN "postalCode" VARCHAR(20),
ADD COLUMN "sex" "Sex",
ALTER COLUMN "email" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "firstName" SET DATA TYPE VARCHAR(100),
ALTER COLUMN "lastName" SET DATA TYPE VARCHAR(100),
ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20),
ALTER COLUMN "preferredLanguage" SET DATA TYPE VARCHAR(10);
-- AlterTable
ALTER TABLE "Review" DROP COLUMN "note",
DROP COLUMN "result",
ADD COLUMN "deletedAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "Staff" ADD COLUMN "email" TEXT,
ADD COLUMN "is_verified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "resetPasswordExpires" TIMESTAMP(3),
ADD COLUMN "resetPasswordToken" TEXT,
ADD COLUMN "send_notif_with_email" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "status" "StaffStatus" NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN "strikes" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "trustScore" INTEGER NOT NULL DEFAULT 100,
DROP COLUMN "role",
ADD COLUMN "role" "StaffRoles" NOT NULL;
-- AlterTable
ALTER TABLE "Users" DROP COLUMN "expertise",
DROP COLUMN "firstName",
DROP COLUMN "lastName",
DROP COLUMN "position",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "expertiseId" INTEGER,
ADD COLUMN "medicalNumber" TEXT,
ADD COLUMN "slug" TEXT NOT NULL,
ADD COLUMN "teamName" TEXT;
-- DropTable
DROP TABLE "Interaction";
-- CreateTable
CREATE TABLE "ReviewTranslation" (
"id" SERIAL NOT NULL,
"reviewId" TEXT NOT NULL,
"lang_id" INTEGER,
"note" TEXT,
"result" TEXT,
CONSTRAINT "ReviewTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UploadSession" (
"id" TEXT NOT NULL,
"uploadKey" TEXT NOT NULL,
"nonce" TEXT NOT NULL,
"used" BOOLEAN NOT NULL DEFAULT false,
"createdById" TEXT,
"purpose" TEXT NOT NULL,
"allowedTypes" TEXT,
"maxSize" INTEGER,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UploadSession_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Violation" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "ViolationType" NOT NULL,
"severity" INTEGER NOT NULL,
"reason" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Violation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Restriction" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "RestrictionType" NOT NULL,
"expiresAt" TIMESTAMP(3),
CONSTRAINT "Restriction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StaffTranslation" (
"id" SERIAL NOT NULL,
"staffId" TEXT NOT NULL,
"lang_id" INTEGER,
"displayName" TEXT,
"position" TEXT,
"description" TEXT,
CONSTRAINT "StaffTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserTranslation" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"lang_id" INTEGER,
"firstName" TEXT,
"lastName" TEXT,
"position" TEXT,
CONSTRAINT "UserTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Expertise" (
"id" SERIAL NOT NULL,
"slug" VARCHAR(100) NOT NULL,
CONSTRAINT "Expertise_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ExpertiseTranslation" (
"id" SERIAL NOT NULL,
"expertiseId" INTEGER NOT NULL,
"lang_id" INTEGER,
"displayName" TEXT,
CONSTRAINT "ExpertiseTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PanelConfig" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"type" "ConfigType" NOT NULL,
"description" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PanelConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Default" (
"id" TEXT NOT NULL DEFAULT 'SITE_DEFAULTS',
"email" TEXT NOT NULL,
"hospitalPhone" TEXT NOT NULL,
"logoUrl" TEXT,
"mapAddress" TEXT NOT NULL,
"instagramLink" TEXT,
"linkedinLink" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Default_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DefaultTranslation" (
"id" SERIAL NOT NULL,
"defaultId" TEXT NOT NULL,
"languageId" INTEGER NOT NULL,
"address" TEXT NOT NULL,
"underLogoText" TEXT,
CONSTRAINT "DefaultTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Page" (
"id" SERIAL NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Page_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PageBlock" (
"id" SERIAL NOT NULL,
"pageId" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"sort" INTEGER NOT NULL,
CONSTRAINT "PageBlock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PageBlockTranslation" (
"id" SERIAL NOT NULL,
"blockId" INTEGER NOT NULL,
"lang_id" INTEGER,
"field" TEXT NOT NULL,
"value" TEXT NOT NULL,
CONSTRAINT "PageBlockTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Language" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"slug" TEXT NOT NULL,
CONSTRAINT "Language_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RefreshToken" (
"id" SERIAL NOT NULL,
"token" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Countries" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"callCode" TEXT NOT NULL,
"coverId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Countries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" SERIAL NOT NULL,
"actorId" TEXT,
"actorRole" "StaffRoles",
"action" "AuditAction" NOT NULL,
"entity" TEXT NOT NULL,
"entityId" INTEGER,
"before" JSONB,
"after" JSONB,
"ip" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccessLog" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"userRole" "StaffRoles" NOT NULL,
"resource" TEXT NOT NULL,
"resourceId" INTEGER,
"reason" TEXT,
"ip" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AccessLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DecisionLog" (
"id" SERIAL NOT NULL,
"decisionType" TEXT NOT NULL,
"input" JSONB NOT NULL,
"output" JSONB NOT NULL,
"algorithmVersion" TEXT NOT NULL,
"actorId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DecisionLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TermsOfService" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"version" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TermsOfService_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TosAcceptanceLog" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"policyType" TEXT NOT NULL,
"policyVersion" TEXT NOT NULL,
"acceptedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ip" TEXT,
CONSTRAINT "TosAcceptanceLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PrivacyPolicy" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PrivacyPolicy_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MedicalPackage" (
"id" SERIAL NOT NULL,
"thumbnail_id" INTEGER,
"icon" TEXT,
"priority" INTEGER,
"parent_id" INTEGER,
CONSTRAINT "MedicalPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MedicalPackagesTranslation" (
"id" SERIAL NOT NULL,
"title" VARCHAR(50) NOT NULL,
"content" TEXT,
"lang_id" INTEGER,
"medicalPackageId" INTEGER,
CONSTRAINT "MedicalPackagesTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TransferPackage" (
"id" SERIAL NOT NULL,
"location" TEXT NOT NULL,
"price" TEXT NOT NULL,
CONSTRAINT "TransferPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TransferPackageTranslations" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"content" TEXT NOT NULL,
"lang_id" INTEGER,
"transferPackageId" INTEGER,
CONSTRAINT "TransferPackageTranslations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TransferTeam" (
"id" SERIAL NOT NULL,
CONSTRAINT "TransferTeam_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TransferTeamTranslation" (
"id" SERIAL NOT NULL,
"lang_id" INTEGER,
"transferTeamId" INTEGER,
"name" VARCHAR(50) NOT NULL,
"introduction" TEXT NOT NULL,
"duties" TEXT NOT NULL,
"services" TEXT NOT NULL,
CONSTRAINT "TransferTeamTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_TransferPackageLocationImages" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_TransferPackageLocationImages_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_TransferPackageGalleryImages" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_TransferPackageGalleryImages_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_TransferPackageToTransferTeam" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_TransferPackageToTransferTeam_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_TransferTeamToUsers" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_TransferTeamToUsers_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "ReviewTranslation_reviewId_lang_id_key" ON "ReviewTranslation"("reviewId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "UploadSession_uploadKey_key" ON "UploadSession"("uploadKey");
-- CreateIndex
CREATE UNIQUE INDEX "UploadSession_nonce_key" ON "UploadSession"("nonce");
-- CreateIndex
CREATE INDEX "UploadSession_createdById_idx" ON "UploadSession"("createdById");
-- CreateIndex
CREATE INDEX "UploadSession_status_idx" ON "UploadSession"("status");
-- CreateIndex
CREATE UNIQUE INDEX "StaffTranslation_staffId_lang_id_key" ON "StaffTranslation"("staffId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "UserTranslation_userId_lang_id_key" ON "UserTranslation"("userId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "ExpertiseTranslation_expertiseId_lang_id_key" ON "ExpertiseTranslation"("expertiseId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "PanelConfig_key_key" ON "PanelConfig"("key");
-- CreateIndex
CREATE UNIQUE INDEX "DefaultTranslation_defaultId_languageId_key" ON "DefaultTranslation"("defaultId", "languageId");
-- CreateIndex
CREATE UNIQUE INDEX "Page_slug_key" ON "Page"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Language_slug_key" ON "Language"("slug");
-- CreateIndex
CREATE INDEX "Language_id_slug_idx" ON "Language"("id", "slug");
-- CreateIndex
CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token");
-- CreateIndex
CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog"("entity", "entityId");
-- CreateIndex
CREATE UNIQUE INDEX "TermsOfService_version_key" ON "TermsOfService"("version");
-- CreateIndex
CREATE UNIQUE INDEX "MedicalPackagesTranslation_medicalPackageId_lang_id_key" ON "MedicalPackagesTranslation"("medicalPackageId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "TransferPackageTranslations_transferPackageId_lang_id_key" ON "TransferPackageTranslations"("transferPackageId", "lang_id");
-- CreateIndex
CREATE UNIQUE INDEX "TransferTeamTranslation_transferTeamId_lang_id_key" ON "TransferTeamTranslation"("transferTeamId", "lang_id");
-- CreateIndex
CREATE INDEX "_TransferPackageLocationImages_B_index" ON "_TransferPackageLocationImages"("B");
-- CreateIndex
CREATE INDEX "_TransferPackageGalleryImages_B_index" ON "_TransferPackageGalleryImages"("B");
-- CreateIndex
CREATE INDEX "_TransferPackageToTransferTeam_B_index" ON "_TransferPackageToTransferTeam"("B");
-- CreateIndex
CREATE INDEX "_TransferTeamToUsers_B_index" ON "_TransferTeamToUsers"("B");
-- CreateIndex
CREATE INDEX "CaseStatusHistory_id_caseId_idx" ON "CaseStatusHistory"("id", "caseId");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_pid_key" ON "Patient"("pid");
-- CreateIndex
CREATE INDEX "Patient_pid_idx" ON "Patient"("pid");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_nationalityCode_key" ON "Patient"("nationalityCode");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_passportCode_nationalityId_key" ON "Patient"("passportCode", "nationalityId");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_phone_key" ON "Patient"("phone");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_email_key" ON "Patient"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Patient_firstName_lastName_birthDate_key" ON "Patient"("firstName", "lastName", "birthDate");
-- CreateIndex
CREATE UNIQUE INDEX "Staff_email_key" ON "Staff"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Users_slug_key" ON "Users"("slug");
-- CreateIndex
CREATE INDEX "Users_id_slug_idx" ON "Users"("id", "slug");
-- AddForeignKey
ALTER TABLE "Patient" ADD CONSTRAINT "Patient_nationalityId_fkey" FOREIGN KEY ("nationalityId") REFERENCES "Countries"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReviewTranslation" ADD CONSTRAINT "ReviewTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReviewTranslation" ADD CONSTRAINT "ReviewTranslation_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "Review"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Violation" ADD CONSTRAINT "Violation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restriction" ADD CONSTRAINT "Restriction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StaffTranslation" ADD CONSTRAINT "StaffTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StaffTranslation" ADD CONSTRAINT "StaffTranslation_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Users" ADD CONSTRAINT "Users_expertiseId_fkey" FOREIGN KEY ("expertiseId") REFERENCES "Expertise"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserTranslation" ADD CONSTRAINT "UserTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserTranslation" ADD CONSTRAINT "UserTranslation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpertiseTranslation" ADD CONSTRAINT "ExpertiseTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpertiseTranslation" ADD CONSTRAINT "ExpertiseTranslation_expertiseId_fkey" FOREIGN KEY ("expertiseId") REFERENCES "Expertise"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DefaultTranslation" ADD CONSTRAINT "DefaultTranslation_defaultId_fkey" FOREIGN KEY ("defaultId") REFERENCES "Default"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DefaultTranslation" ADD CONSTRAINT "DefaultTranslation_languageId_fkey" FOREIGN KEY ("languageId") REFERENCES "Language"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PageBlock" ADD CONSTRAINT "PageBlock_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PageBlockTranslation" ADD CONSTRAINT "PageBlockTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PageBlockTranslation" ADD CONSTRAINT "PageBlockTranslation_blockId_fkey" FOREIGN KEY ("blockId") REFERENCES "PageBlock"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Countries" ADD CONSTRAINT "Countries_coverId_fkey" FOREIGN KEY ("coverId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TosAcceptanceLog" ADD CONSTRAINT "TosAcceptanceLog_policyVersion_fkey" FOREIGN KEY ("policyVersion") REFERENCES "TermsOfService"("version") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MedicalPackage" ADD CONSTRAINT "MedicalPackage_thumbnail_id_fkey" FOREIGN KEY ("thumbnail_id") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MedicalPackage" ADD CONSTRAINT "MedicalPackage_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "MedicalPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MedicalPackagesTranslation" ADD CONSTRAINT "MedicalPackagesTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MedicalPackagesTranslation" ADD CONSTRAINT "MedicalPackagesTranslation_medicalPackageId_fkey" FOREIGN KEY ("medicalPackageId") REFERENCES "MedicalPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TransferPackageTranslations" ADD CONSTRAINT "TransferPackageTranslations_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TransferPackageTranslations" ADD CONSTRAINT "TransferPackageTranslations_transferPackageId_fkey" FOREIGN KEY ("transferPackageId") REFERENCES "TransferPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TransferTeamTranslation" ADD CONSTRAINT "TransferTeamTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TransferTeamTranslation" ADD CONSTRAINT "TransferTeamTranslation_transferTeamId_fkey" FOREIGN KEY ("transferTeamId") REFERENCES "TransferTeam"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageLocationImages" ADD CONSTRAINT "_TransferPackageLocationImages_A_fkey" FOREIGN KEY ("A") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageLocationImages" ADD CONSTRAINT "_TransferPackageLocationImages_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageGalleryImages" ADD CONSTRAINT "_TransferPackageGalleryImages_A_fkey" FOREIGN KEY ("A") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageGalleryImages" ADD CONSTRAINT "_TransferPackageGalleryImages_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageToTransferTeam" ADD CONSTRAINT "_TransferPackageToTransferTeam_A_fkey" FOREIGN KEY ("A") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferPackageToTransferTeam" ADD CONSTRAINT "_TransferPackageToTransferTeam_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferTeam"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferTeamToUsers" ADD CONSTRAINT "_TransferTeamToUsers_A_fkey" FOREIGN KEY ("A") REFERENCES "TransferTeam"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TransferTeamToUsers" ADD CONSTRAINT "_TransferTeamToUsers_B_fkey" FOREIGN KEY ("B") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

682
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,682 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
enum CaseStatus {
NEW
CONTACTED
DOCS_PENDING
REVIEWING
PRE_APPROVED
REJECTED
CLOSED
CONVERTED_TO_HIS
}
// enum DocumentType {
// PASSPORT
// MEDICAL_RECORD
// IMAGING
// LAB_RESULT
// OTHER
// }
enum InteractionType {
PHONE
WHATSAPP
EMAIL
SYSTEM
}
model Patient {
id String @id @default(uuid())
pid String @unique @db.VarChar(10)
// -------- Identity --------
firstName String @db.VarChar(100)
lastName String @db.VarChar(100)
birthDate DateTime?
sex Sex?
age Int?
// -------- Nationality --------
nationality Countries? @relation(fields: [nationalityId], references: [id])
nationalityId Int?
nationalityCode String? @db.Char(3)
passportCode String? @db.VarChar(20)
// -------- Contact --------
phone String? @db.VarChar(20)
email String? @db.VarChar(255)
preferredLanguage String? @db.VarChar(10)
// -------- Address --------
address String? @db.VarChar(500)
postalCode String? @db.VarChar(20)
// -------- System --------
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// -------- Relations --------
cases OnlineCase[]
documents Document[]
// -------- Indexes & Constraints --------
@@unique([nationalityCode]) // کد ملی
@@unique([passportCode, nationalityId]) // پاسپورت
@@unique([phone])
@@unique([email])
@@unique([firstName, lastName, birthDate])
@@index([pid])
}
model Document {
id String @id @default(uuid())
caseId String?
patientId String?
uploadedById String?
type DocumentType
filename String
fileUrl String
fileKey String
mimeType String?
size Int?
checksum String?
status DocStatus @default(NEW)
is_deleted Boolean @default(false)
createdAt DateTime @default(now())
case OnlineCase? @relation(fields: [caseId], references: [id])
patient Patient? @relation(fields: [patientId], references: [id])
uploadedBy Staff? @relation(fields: [uploadedById], references: [id])
}
enum DocumentType {
MEDICAL_IMAGE
LAB_RESULT
PRESCRIPTION
PASSPORT
NATIONAL_ID
OTHER
OTHER_FILE
}
enum DocStatus {
NEW
PENDING_SCAN
SAFE
INFECTED
INVALID
}
enum Sex {
male
female
other
}
model OnlineCase {
id String @id @default(uuid())
patientId String
trackingCode String @unique
message String?
specialty String?
formData Json?
status CaseStatus @default(NEW)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
patient Patient @relation(fields: [patientId], references: [id])
documents Document[]
// interactions Interaction[]
reviews Review[]
statusHistory CaseStatusHistory[]
}
model Review {
id String @id @default(uuid())
caseId String
doctorId String?
createdAt DateTime @default(now())
deletedAt DateTime?
case OnlineCase @relation(fields: [caseId], references: [id])
doctor Staff? @relation(fields: [doctorId], references: [id])
translations ReviewTranslation[]
}
model ReviewTranslation {
id Int @id @default(autoincrement())
reviewId String
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
note String?
result String? // eligible, needs more docs, not eligible (می‌تونه ترجمه شود)
review Review @relation(fields: [reviewId], references: [id])
@@unique([reviewId, lang_id]) // هر Review در هر زبان فقط یک ترجمه دارد
}
model CaseStatusHistory {
id String @id @default(uuid())
caseId String
from CaseStatus?
to CaseStatus?
changedBy String?
createdAt DateTime @default(now())
case OnlineCase @relation(fields: [caseId], references: [id])
@@index([id, caseId])
}
model UploadSession {
id String @id @default(uuid())
uploadKey String @unique // path/key to store file on CDN (e.g. documents/{uuid}.pdf)
nonce String @unique
used Boolean @default(false)
createdById String? // user id
purpose String // "document" | "image"
allowedTypes String? // JSON string array
maxSize Int?
status String @default("PENDING")
createdAt DateTime @default(now())
expiresAt DateTime
// indexes
@@index([createdById])
@@index([status])
}
enum UploadStatus {
PENDING
UPLOADING
UPLOADED
VERIFIED
FAILED
EXPIRED
}
enum StaffRoles {
developer
admin
doctor
coordinator
}
model Staff {
id String @id @default(uuid())
username String @unique
password String
email String? @unique
role StaffRoles
is_verified Boolean @default(false)
send_notif_with_email Boolean @default(false)
resetPasswordToken String?
resetPasswordExpires DateTime?
status StaffStatus @default(ACTIVE)
trustScore Int @default(100)
strikes Int @default(0)
restrictions Restriction[]
violations Violation[]
documents Document[]
reviews Review[]
profilePicture Image? @relation(fields: [profilePictureID],references: [id])
profilePictureID Int?
translations StaffTranslation[]
tokens RefreshToken[]
}
enum StaffStatus {
ACTIVE
WARNED
RESTRICTED
BANNED
}
model Violation {
id String @id @default(uuid())
userId String
type ViolationType
severity Int
reason String
user Staff @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
}
enum ViolationType {
SPAM
RATE_LIMIT
CONTENT
ABUSE
}
model Restriction {
id String @id @default(uuid())
userId String
type RestrictionType
expiresAt DateTime?
user Staff @relation(fields: [userId], references: [id])
}
enum RestrictionType {
TEMP
SHADOW
PERMANENT
}
model StaffTranslation {
id Int @id @default(autoincrement())
staffId String
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
// فیلدهایی که نیاز به ترجمه دارند
displayName String?
position String?
description String?
staff Staff @relation(fields: [staffId], references: [id])
@@unique([staffId, lang_id]) // هر کارمند در هر زبان فقط یک ترجمه دارد
}
model Image {
id Int @id @default(autoincrement())
fileKey String
filename String?
fileUrl String?
mimeType String?
size Int?
usersProfile Users[]
countriesCover Countries[]
medicalPackagesThumbnails MedicalPackage[]
transferPackageLocationImages TransferPackage[] @relation(name: "TransferPackageLocationImages")
transferPackageGalleryImages TransferPackage[] @relation(name: "TransferPackageGalleryImages")
staffProfilePictures Staff[]
}
model Users {
id Int @id @default(autoincrement())
slug String @unique
type UsersType @default(DOCTOR)
displayInMainPage Boolean @default(false)
phone String?
email String?
teamName String?
medicalNumber String?
expertise Expertise? @relation(fields: [expertiseId], references: [id])
expertiseId Int?
image Image? @relation(fields: [imageId], references: [id])
imageId Int?
createdAt DateTime @default(now())
translations UserTranslation[]
transerTeamMembers TransferTeam[]
@@index([id, slug])
}
model UserTranslation {
id Int @id @default(autoincrement())
userId Int
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
firstName String?
lastName String?
position String?
bio String?
excerpt String?
user Users @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد
}
model Expertise {
id Int @id @default(autoincrement())
slug String @db.VarChar(100)
users Users[]
translations ExpertiseTranslation[]
}
model ExpertiseTranslation {
id Int @id @default(autoincrement())
expertiseId Int
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
displayName String?
level EducationLevel?
expertise Expertise @relation(fields: [expertiseId], references: [id])
@@unique([expertiseId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد
}
enum EducationLevel {
GP
SPECIALIST
SUBSPECIALIST
FELLOWSHIP
}
enum UsersType {
DOCTOR
TRANSFER_TEAM
DEPARTMENT
}
model PanelConfig {
id String @id @default(uuid())
key String @unique
value String // مقدار خام (string)
type ConfigType // نوع داده
description String?
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
model Default {
id String @id @default("SITE_DEFAULTS")
email String
hospitalPhone String
logoUrl String?
mapAddress String
instagramLink String?
linkedinLink String?
ipdNumber String?
updatedAt DateTime @updatedAt
translations DefaultTranslation[]
}
model DefaultTranslation {
id Int @id @default(autoincrement())
default Default @relation(fields: [defaultId], references: [id])
defaultId String
language Language @relation(fields: [languageId], references: [id])
languageId Int
address String
underLogoText String?
aboutUsText String?
patientsRights String?
@@unique([defaultId, languageId])
}
model Statics {
id String @id @default(uuid())
key String @unique @db.VarChar(191)
group String @db.VarChar(100)
description String? @db.Text
translations StaticsTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([group])
}
model StaticsTranslation {
id String @id @default(uuid())
languageId Int
staticsKeyId String
value String @db.Text
language Language @relation(fields: [languageId], references: [id], onDelete: Cascade)
staticsKey Statics @relation(fields: [staticsKeyId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([languageId, staticsKeyId])
@@index([languageId])
@@index([staticsKeyId])
}
enum ConfigType {
BOOLEAN
NUMBER
STRING
JSON
STRING_ARRAY
}
model Page {
id Int @id @default(autoincrement())
slug String @unique
createdAt DateTime @default(now())
pageBlocks PageBlock[]
}
model PageBlock {
id Int @id @default(autoincrement())
pageId Int
type String // hero, text, image, faq, etc
sort Int
page Page @relation(fields: [pageId], references: [id])
pageBlockTranslations PageBlockTranslation[]
}
model PageBlockTranslation {
id Int @id @default(autoincrement())
blockId Int
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
field String // title, subtitle, description, button_text
value String
block PageBlock @relation(fields: [blockId], references: [id])
}
model Language {
id Int @id @default(autoincrement())
title String
slug String @unique
pageBlockTranslations PageBlockTranslation[]
// translations Translation[]
reviewTranslations ReviewTranslation[]
staffTranslations StaffTranslation[]
userTranslations UserTranslation[]
expertiseTranslations ExpertiseTranslation[]
defaultTranslations DefaultTranslation[]
medicalPackagesTranslations MedicalPackagesTranslation[]
transferPackageTranslations TransferPackageTranslations[]
transferTeamTranslations TransferTeamTranslation[]
staticsTranslations StaticsTranslation[]
@@index([id, slug])
}
model RefreshToken {
id Int @id @default(autoincrement())
token String @unique
staff Staff @relation(fields: [staffId], references: [id])
staffId String
expiresAt DateTime
createdAt DateTime @default(now())
}
model Countries {
id Int @id @default(autoincrement())
name String
callCode String
cover Image? @relation(fields: [coverId], references: [id])
coverId Int?
createdAt DateTime @default(now())
patients Patient[]
}
enum AuditAction {
CREATE
UPDATE
DELETE
READ
}
model AuditLog {
id Int @id @default(autoincrement())
actorId String?
actorRole StaffRoles?
action AuditAction
entity String
entityId Int?
before Json?
after Json?
ip String?
createdAt DateTime @default(now())
@@index([entity, entityId])
}
model AccessLog {
id Int @id @default(autoincrement())
userId String
userRole StaffRoles
resource String
resourceId Int?
reason String?
ip String?
createdAt DateTime @default(now())
}
model DecisionLog {
id Int @id @default(autoincrement())
decisionType String
input Json
output Json
algorithmVersion String
actorId String?
createdAt DateTime @default(now())
}
model TermsOfService {
id String @id @default(uuid()) // شناسه یکتا
title String // عنوان سند، مثلا "Terms of Service"
content String // متن کامل TOS (Markdown یا HTML)
version String @unique // نسخه سند، مثلا "v1.0.0"
isActive Boolean @default(true) // آیا این نسخه فعال است؟
createdAt DateTime @default(now()) // زمان ایجاد نسخه
updatedAt DateTime @updatedAt // زمان آخرین آپدیت
/// روابط اختیاری، اگر بخواهید لاگ پذیرش کاربران را نگه دارید
acceptances TosAcceptanceLog[] // ثبت کاربرانی که این نسخه را پذیرفته‌اند
}
model TosAcceptanceLog {
id Int @id @default(autoincrement())
userId String // شناسه کاربر
policyType String // نوع سند (TOS یا Privacy Policy)
policyVersion String // نسخه سند پذیرفته شده
acceptedAt DateTime @default(now()) // زمان پذیرش
ip String? // آی‌پی کاربر برای ثبت لاگ
tos TermsOfService? @relation(fields: [policyVersion], references: [version])
}
model PrivacyPolicy {
id Int @id @default(autoincrement()) // شناسه یکتا
content String // متن کامل TOS (Markdown یا HTML)
createdAt DateTime @default(now()) // زمان ایجاد نسخه
updatedAt DateTime @updatedAt // زمان آخرین آپدیت
}
model MedicalPackage {
id Int @id @default(autoincrement())
thumbnail Image? @relation(fields: [thumbnail_id], references: [id])
thumbnail_id Int?
icon String?
priority Int?
price String
parent MedicalPackage? @relation("MedicalPackageHierarchy", fields: [parent_id], references: [id], onDelete: SetNull)
parent_id Int? // برای اشاره به دسته‌بندی والد
children MedicalPackage[] @relation("MedicalPackageHierarchy")
translations MedicalPackagesTranslation[]
}
model MedicalPackagesTranslation {
id Int @id @default(autoincrement())
title String @db.VarChar(50)
content String?
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
medicalPackage MedicalPackage? @relation(fields: [medicalPackageId], references: [id])
medicalPackageId Int?
@@unique([medicalPackageId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد
}
model TransferPackage {
id Int @id @default(autoincrement())
location String
price String
locationImages Image[] @relation(name: "TransferPackageLocationImages")
galleryImages Image[] @relation(name: "TransferPackageGalleryImages")
transferTeam TransferTeam[]
translations TransferPackageTranslations[]
}
model TransferPackageTranslations {
id Int @id @default(autoincrement())
name String
content String
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
transferPackage TransferPackage? @relation(fields: [transferPackageId], references: [id])
transferPackageId Int?
@@unique([transferPackageId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد
}
model TransferTeam {
id Int @id @default(autoincrement())
members Users[]
packages TransferPackage[]
translations TransferTeamTranslation[]
}
model TransferTeamTranslation {
id Int @id @default(autoincrement())
lang Language? @relation(fields: [lang_id], references: [id])
lang_id Int?
transferTeam TransferTeam? @relation(fields: [transferTeamId], references: [id])
transferTeamId Int?
name String @db.VarChar(50)
introduction String
duties String
services String
@@unique([transferTeamId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد
}

90
src/app/server.ts Normal file
View File

@@ -0,0 +1,90 @@
import dotenv from "dotenv";
import path from "path";
import cookieParser from "cookie-parser";
import {NextFunction, Request, Response} from "express";
import createHttpError from "http-errors";
import {secureApp} from "@/core/secure-app";
import mainRouter from "@/core/router/main.router";
import {ServerErrorsObject} from "@/common/types";
// import {connectRabbit} from "../common/configs/rabbit";
import {connectMongo} from "../../mongodb/config/index";
import {
ErrorLog,
} from "../../mongodb/models/index";
import {apiLoggingMiddleware} from "@/core/middlewares/logs.middleware";
const express = require("express") as typeof import("express");
dotenv.config();
export default class ServerApplication {
#PORT = process.env.PORT || 3500;
#APP = express();
constructor() {
this.AppConfiguration();
this.StartApplication();
this.InitClientSession();
this.RoutesConfiguration();
this.ErrorHandlingConfiguration();
}
async AppConfiguration() {
this.#APP.use(secureApp);
this.#APP.use(express.json());
this.#APP.use(express.urlencoded({extended: true}));
this.#APP.set("json spaces", 2);
this.#APP.use(
"/resources/images",
express.static(path.join(__dirname, "..", "media", "images"))
);
this.#APP.use(
"/resources/videos",
express.static(path.join(__dirname, "..", "media", "videos"))
);
}
async StartApplication() {
// await database_connection();
// await connectRabbit();
await connectMongo();
this.#APP.listen(this.#PORT, () => {
console.log(
`Server Running on PORT ${this.#PORT} url : ${"http://localhost:"}${
this.#PORT
}`
);
});
}
InitClientSession() {
this.#APP.use(cookieParser(process.env.COOKIE_PARSER_SECRET_KEY));
}
RoutesConfiguration() {
this.#APP.use(apiLoggingMiddleware());
this.#APP.get("/", (req, res) => res.send(""));
this.#APP.use("/api/v1", mainRouter);
}
ErrorHandlingConfiguration() {
this.#APP.use((req: any, res: Response, next: NextFunction) => {
next(createHttpError.NotFound("این آدرس یافت نشد"));
});
this.#APP.use(
async (error: any, req: Request, res: Response, next: NextFunction) => {
await ErrorLog.create({
message: error.message,
stack: error.stack,
severity: "HIGH",
});
const serverError = createHttpError.InternalServerError();
const statusCode = error.status || serverError.status;
const message: string = error.message || serverError.message;
const errorObject: ServerErrorsObject = {
status: statusCode,
error: {
message,
},
};
return res.status(statusCode).json(errorObject);
}
);
}
}

View File

@@ -0,0 +1,40 @@
import multer, { FileFilterCallback } from "multer";
import { Request } from "express";
// Accepted document mime types
const allowedDocTypes: string[] = [
"application/pdf",
"application/zip",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream", // For DICOM files sometimes
];
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/documents");
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
// Main multer config
export const uploadDocument = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10 MB
},
fileFilter: (
req: Request,
file: Express.Multer.File,
cb: FileFilterCallback
) => {
if (!allowedDocTypes.includes(file.mimetype)) {
return cb(
new Error("فرمت فایل معتبر نیست (فقط PDF, ZIP, RAR, DICOM)")
);
}
cb(null, true);
},
});

View File

@@ -0,0 +1,33 @@
import multer, { FileFilterCallback } from "multer";
import { Request } from "express";
// Allowed image formats
const allowedImageTypes: string[] = ["image/jpeg", "image/png", "image/webp"];
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/profileImages");
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
export const uploadProfileImage = multer({
storage,
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB
},
fileFilter: (
req: Request,
file: Express.Multer.File,
cb: FileFilterCallback
) => {
if (!allowedImageTypes.includes(file.mimetype)) {
return cb(
new Error("فقط فرمت‌های JPG, PNG, WEBP برای تصویر پروفایل معتبر است")
);
}
cb(null, true);
},
});

View File

@@ -0,0 +1,141 @@
import {CorsOptions} from "cors";
import dotenv from "dotenv";
import {NextFunction} from "express";
import {Options} from "express-rate-limit";
import {HelmetOptions} from "helmet";
import createHttpError from "http-errors";
import {VALID_ORIGINS} from "./variables";
import {Request} from "express";
dotenv.config();
const multer = require("multer");
const path = require("path");
const fs = require("fs");
export const config = {
port: process.env.PORT || 3500,
db: {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
jwtSecret: process.env.JWT_SECRET || "secret",
};
export const limiter_option: Partial<Options> = {
windowMs: 15 * 60 * 1000,
max: 4000,
standardHeaders: true,
legacyHeaders: false,
handler: (req: any, res: any, next: NextFunction) => {
next(
new createHttpError.TooManyRequests(
"تعداد درخواست شما بیشتر از حد مجاز است ، در زمان دیگری مجدد درخواست دهید"
)
);
},
// message: {error: "Too many requests, please try again later."},
};
export const helmet_option: HelmetOptions = {
contentSecurityPolicy: {
useDefaults: true,
directives: {
defaultSrc: ["'self'"],
// scriptSrc: ["'self'", "'unsafe-inline'", "https://trusted.cdn.com"],
// styleSrc: ["'self'", "'unsafe-inline'"],
// imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
// fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [], // تبدیل اتومات http به https
},
},
crossOriginEmbedderPolicy: true,
crossOriginResourcePolicy: {policy: "same-origin"},
frameguard: {action: "deny"}, // جلوگیری از Clickjacking
referrerPolicy: {policy: "no-referrer"}, // جلوگیری از لو رفتن referrer
xssFilter: true, // فعال کردن فیلتر XSS
hsts: {maxAge: 63072000, includeSubDomains: true, preload: true}, // HSTS
};
export const cors_option: CorsOptions = {
origin: true,
credentials: true,
allowedHeaders: [
"Content-Type",
"Authorization",
"x-upload-token", // 👈 اینو اضافه کن
],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
maxAge: 600,
// origin:["http://localhost:3000"]
};
async function createDirectoryRoute(req: Request<any>) {
const date = new Date();
const directory = path.join(__dirname, "..", "..", "..", "public", "images");
req.body.fileUploadPath = path.join(directory, "original");
fs.mkdirSync(directory, {recursive: true});
return directory;
}
// const storage = multer.diskStorage({
// destination: async (
// req: Request<any>,
// file: any,
// cb: (a?: any, b?: any) => void
// ) => {
// if (file?.originalname) {
// const filePath = await createDirectoryRoute(req);
// return cb(null, filePath);
// }
// cb(null, null);
// },
// filename: (req: Request<any>, file: any, cb: (a?: any, b?: any) => void) => {
// if (file?.originalname) {
// const ext = path.extname(file.originalname);
// const fileName = String(new Date().getTime() + ext);
// req.body.filename = fileName;
// return cb(null, fileName);
// }
// cb(null, null);
// },
// });
// function fileFilter(
// req: Request<any>,
// file: any,
// cb: (a?: any, b?: any) => void
// ) {
// const ext = path.extname(file.originalname);
// if (ValidExtNames.includes(ext)) {
// return cb(null, true);
// }
// return cb(createHttpError.BadRequest("فرمت ارسالی صحیح نمی باشد."));
// }
// const storage = multer.diskStorage({
// destination: async (req: Request<any>, file: any, cb: Function) => {
// // const root_directory = await createDirectoryRoute(req);
// // const originalDirectory = path.join(root_directory, "original");
// // مشخص کردن پوشه‌ای که فایل‌ها ذخیره شوند
// cb(null, true); // پوشه اصلی
// },
// filename: (req: Request<any>, file: any, cb: Function) => {
// cb(null, file.filename); // نام فایل بر اساس زمان فعلی
// },
// });
// const uploadFile = multer({
// storage: storage,
// // fileFilter,
// // limits: {fileSize: 1000000},
// });
const storage = multer.memoryStorage(); // Keep files in memory (instead of disk)
const uploadFile = multer({storage: storage});
export {uploadFile};

View File

View File

@@ -0,0 +1,64 @@
import {CookieOptions} from "express";
// interface registration_policy_types {
// user: {
// defaultStatus: UserSt;
// emailVerificationRequired: boolean;
// otpVerificationRequired: boolean;
// };
// business: {
// defaultStatus: UserSt;
// emailVerificationRequired: boolean;
// otpVerificationRequired: boolean;
// adminApprovalRequired: boolean;
// callApprovalRequired: boolean;
// };
// }
// export const registration_policy: Readonly<registration_policy_types> =
// Object.freeze({
// user: {
// defaultStatus: "active" as const,
// emailVerificationRequired: false,
// otpVerificationRequired: true,
// },
// business: {
// defaultStatus: "suspended" as const,
// emailVerificationRequired: false,
// otpVerificationRequired: true,
// adminApprovalRequired: true,
// callApprovalRequired: true,
// },
// });
interface token_policy_types {
r_token_expire: `${number}${"s" | "m" | "h" | "d" | "y"}`;
a_token_expire: `${number}${"s" | "m" | "h" | "d" | "y"}`;
access_token_options: CookieOptions;
refresh_token_options: CookieOptions;
}
const isProd = process.env.NODE_ENV === "production";
console.log(isProd)
export const token_policy: Readonly<token_policy_types> = Object.freeze({
r_token_expire: "7d",
a_token_expire: "1h",
access_token_options: {
httpOnly: true,
signed: true,
secure: isProd,
sameSite: isProd ? "none" : "lax",
maxAge: 60 * 60 * 1000, // 1 ساعت
} as CookieOptions,
refresh_token_options: {
httpOnly: true,
signed: true,
secure: isProd,
sameSite: isProd ? "none" : "lax",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 روز
} as const,
});
export const otp_code_length = 6;
export const price_policy_max = 990000000000;

View File

@@ -0,0 +1,4 @@
export const mobile_regex = /^(\+98|0)?9\d{9}$/;
export const otp_code_regex = /^\d{6}$/;
export const password_regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

View File

@@ -0,0 +1,228 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.allCountries = exports.SELECT_STAFF_OUT_DATA = exports.VALID_ORIGINS = void 0;
// export const projectName="shomalhospital"
exports.VALID_ORIGINS = [
"http://localhost:3000",
"https://ipd.shomalhospital.com",
"http://ipd.shomalhospital.com",
];
exports.SELECT_STAFF_OUT_DATA = Object.freeze({
id: true,
username: true,
email: true,
is_verified: true,
role: true,
send_notif_with_email: true,
});
exports.allCountries = [
{ label: "Afghanistan", value: "afghanistan", code: "+93" },
{ label: "Albania", value: "albania", code: "+355" },
{ label: "Algeria", value: "algeria", code: "+213" },
{ label: "American Samoa", value: "american-samoa", code: "+1-684" },
{ label: "Andorra", value: "andorra", code: "+376" },
{ label: "Angola", value: "angola", code: "+244" },
{ label: "Anguilla", value: "anguilla", code: "+1-264" },
{ label: "Antigua and Barbuda", value: "antigua-and-barbuda", code: "+1-268" },
{ label: "Argentina", value: "argentina", code: "+54" },
{ label: "Armenia", value: "armenia", code: "+374" },
{ label: "Aruba", value: "aruba", code: "+297" },
{ label: "Australia", value: "australia", code: "+61" },
{ label: "Austria", value: "austria", code: "+43" },
{ label: "Azerbaijan", value: "azerbaijan", code: "+994" },
{ label: "Bahamas", value: "bahamas", code: "+1-242" },
{ label: "Bahrain", value: "bahrain", code: "+973" },
{ label: "Bangladesh", value: "bangladesh", code: "+880" },
{ label: "Barbados", value: "barbados", code: "+1-246" },
{ label: "Belarus", value: "belarus", code: "+375" },
{ label: "Belgium", value: "belgium", code: "+32" },
{ label: "Belize", value: "belize", code: "+501" },
{ label: "Benin", value: "benin", code: "+229" },
{ label: "Bhutan", value: "bhutan", code: "+975" },
{ label: "Bolivia", value: "bolivia", code: "+591" },
{
label: "Bosnia and Herzegovina",
value: "bosnia-and-herzegovina",
code: "+387",
},
{ label: "Botswana", value: "botswana", code: "+267" },
{ label: "Brazil", value: "brazil", code: "+55" },
{ label: "Brunei", value: "brunei", code: "+673" },
{ label: "Bulgaria", value: "bulgaria", code: "+359" },
{ label: "Burkina Faso", value: "burkina-faso", code: "+226" },
{ label: "Burundi", value: "burundi", code: "+257" },
{ label: "Cabo Verde", value: "cabo-verde", code: "+238" },
{ label: "Cambodia", value: "cambodia", code: "+855" },
{ label: "Cameroon", value: "cameroon", code: "+237" },
{ label: "Canada", value: "canada", code: "+1" },
{
label: "Central African Republic",
value: "central-african-republic",
code: "+236",
},
{ label: "Chad", value: "chad", code: "+235" },
{ label: "Chile", value: "chile", code: "+56" },
{ label: "China", value: "china", code: "+86" },
{ label: "Colombia", value: "colombia", code: "+57" },
{ label: "Comoros", value: "comoros", code: "+269" },
{ label: "Congo", value: "congo", code: "+242" },
{
label: "Congo, Democratic Republic of the",
value: "congo-democratic-republic",
code: "+243",
},
{ label: "Costa Rica", value: "costa-rica", code: "+506" },
{ label: "Croatia", value: "croatia", code: "+385" },
{ label: "Cuba", value: "cuba", code: "+53" },
{ label: "Cyprus", value: "cyprus", code: "+357" },
{ label: "Czech Republic", value: "czech-republic", code: "+420" },
{ label: "Denmark", value: "denmark", code: "+45" },
{ label: "Djibouti", value: "djibouti", code: "+253" },
{ label: "Dominica", value: "dominica", code: "+1-767" },
{ label: "Dominican Republic", value: "dominican-republic", code: "+1-809" },
{ label: "Ecuador", value: "ecuador", code: "+593" },
{ label: "Egypt", value: "egypt", code: "+20" },
{ label: "El Salvador", value: "el-salvador", code: "+503" },
{ label: "Equatorial Guinea", value: "equatorial-guinea", code: "+240" },
{ label: "Eritrea", value: "eritrea", code: "+291" },
{ label: "Estonia", value: "estonia", code: "+372" },
{ label: "Eswatini", value: "eswatini", code: "+268" },
{ label: "Ethiopia", value: "ethiopia", code: "+251" },
{ label: "Fiji", value: "fiji", code: "+679" },
{ label: "Finland", value: "finland", code: "+358" },
{ label: "France", value: "france", code: "+33" },
{ label: "Gabon", value: "gabon", code: "+241" },
{ label: "Gambia", value: "gambia", code: "+220" },
{ label: "Georgia", value: "georgia", code: "+995" },
{ label: "Germany", value: "germany", code: "+49" },
{ label: "Ghana", value: "ghana", code: "+233" },
{ label: "Greece", value: "greece", code: "+30" },
{ label: "Grenada", value: "grenada", code: "+1-473" },
{ label: "Guatemala", value: "guatemala", code: "+502" },
{ label: "Guinea", value: "guinea", code: "+224" },
{ label: "Guinea-Bissau", value: "guinea-bissau", code: "+245" },
{ label: "Guyana", value: "guyana", code: "+592" },
{ label: "Haiti", value: "haiti", code: "+509" },
{ label: "Honduras", value: "honduras", code: "+504" },
{ label: "Hungary", value: "hungary", code: "+36" },
{ label: "Iceland", value: "iceland", code: "+354" },
{ label: "India", value: "india", code: "+91" },
{ label: "Indonesia", value: "indonesia", code: "+62" },
{ label: "Iran", value: "iran", code: "+98" },
{ label: "Iraq", value: "iraq", code: "+964" },
{ label: "Ireland", value: "ireland", code: "+353" },
{ label: "Israel", value: "israel", code: "+972" },
{ label: "Italy", value: "italy", code: "+39" },
{ label: "Jamaica", value: "jamaica", code: "+1-876" },
{ label: "Japan", value: "japan", code: "+81" },
{ label: "Jordan", value: "jordan", code: "+962" },
{ label: "Kazakhstan", value: "kazakhstan", code: "+7" },
{ label: "Kenya", value: "kenya", code: "+254" },
{ label: "Kiribati", value: "kiribati", code: "+686" },
{ label: "Kuwait", value: "kuwait", code: "+965" },
{ label: "Kyrgyzstan", value: "kyrgyzstan", code: "+996" },
{ label: "Laos", value: "laos", code: "+856" },
{ label: "Latvia", value: "latvia", code: "+371" },
{ label: "Lebanon", value: "lebanon", code: "+961" },
{ label: "Lesotho", value: "lesotho", code: "+266" },
{ label: "Liberia", value: "liberia", code: "+231" },
{ label: "Libya", value: "libya", code: "+218" },
{ label: "Liechtenstein", value: "liechtenstein", code: "+423" },
{ label: "Lithuania", value: "lithuania", code: "+370" },
{ label: "Luxembourg", value: "luxembourg", code: "+352" },
{ label: "Macau", value: "macau", code: "+853" },
{ label: "North Macedonia", value: "north-macedonia", code: "+389" },
{ label: "Madagascar", value: "madagascar", code: "+261" },
{ label: "Malawi", value: "malawi", code: "+265" },
{ label: "Malaysia", value: "malaysia", code: "+60" },
{ label: "Maldives", value: "maldives", code: "+960" },
{ label: "Mali", value: "mali", code: "+223" },
{ label: "Malta", value: "malta", code: "+356" },
{ label: "Marshall Islands", value: "marshall-islands", code: "+692" },
{ label: "Mauritania", value: "mauritania", code: "+222" },
{ label: "Mauritius", value: "mauritius", code: "+230" },
{ label: "Mexico", value: "mexico", code: "+52" },
{ label: "Micronesia", value: "micronesia", code: "+691" },
{ label: "Moldova", value: "moldova", code: "+373" },
{ label: "Monaco", value: "monaco", code: "+377" },
{ label: "Mongolia", value: "mongolia", code: "+976" },
{ label: "Montenegro", value: "montenegro", code: "+382" },
{ label: "Morocco", value: "morocco", code: "+212" },
{ label: "Mozambique", value: "mozambique", code: "+258" },
{ label: "Myanmar", value: "myanmar", code: "+95" },
{ label: "Namibia", value: "namibia", code: "+264" },
{ label: "Nauru", value: "nauru", code: "+674" },
{ label: "Nepal", value: "nepal", code: "+977" },
{ label: "Netherlands", value: "netherlands", code: "+31" },
{ label: "NewZealand", value: "new-zealand", code: "+64" },
{ label: "Nicaragua", value: "nicaragua", code: "+505" },
{ label: "Niger", value: "niger", code: "+227" },
{ label: "Nigeria", value: "nigeria", code: "+234" },
{ label: "Norway", value: "norway", code: "+47" },
{ label: "Oman", value: "oman", code: "+968" },
{ label: "Pakistan", value: "pakistan", code: "+92" },
{ label: "Palau", value: "palau", code: "+680" },
{ label: "Palestine", value: "palestine", code: "+970" },
{ label: "Panama", value: "panama", code: "+507" },
{ label: "Papua New Guinea", value: "papua-new-guinea", code: "+675" },
{ label: "Paraguay", value: "paraguay", code: "+595" },
{ label: "Peru", value: "peru", code: "+51" },
{ label: "Philippines", value: "philippines", code: "+63" },
{ label: "Poland", value: "poland", code: "+48" },
{ label: "Portugal", value: "portugal", code: "+351" },
{ label: "Qatar", value: "qatar", code: "+974" },
{ label: "Romania", value: "romania", code: "+40" },
{ label: "Russia", value: "russia", code: "+7" },
{ label: "Rwanda", value: "rwanda", code: "+250" },
{ label: "Samoa", value: "samoa", code: "+685" },
{ label: "San Marino", value: "san-marino", code: "+378" },
{ label: "Saudi Arabia", value: "saudi-arabia", code: "+966" },
{ label: "Senegal", value: "senegal", code: "+221" },
{ label: "Serbia", value: "serbia", code: "+381" },
{ label: "Seychelles", value: "seychelles", code: "+248" },
{ label: "Sierra Leone", value: "sierra-leone", code: "+232" },
{ label: "Singapore", value: "singapore", code: "+65" },
{ label: "Slovakia", value: "slovakia", code: "+421" },
{ label: "Slovenia", value: "slovenia", code: "+386" },
{ label: "Solomon Islands", value: "solomon-islands", code: "+677" },
{ label: "Somalia", value: "somalia", code: "+252" },
{ label: "South Africa", value: "south-africa", code: "+27" },
{ label: "South Sudan", value: "south-sudan", code: "+211" },
{ label: "Spain", value: "spain", code: "+34" },
{ label: "Sri Lanka", value: "sri-lanka", code: "+94" },
{ label: "Sudan", value: "sudan", code: "+249" },
{ label: "Suriname", value: "suriname", code: "+597" },
{ label: "Sweden", value: "sweden", code: "+46" },
{ label: "Switzerland", value: "switzerland", code: "+41" },
{ label: "Syria", value: "syria", code: "+963" },
{ label: "Taiwan", value: "taiwan", code: "+886" },
{ label: "Tajikistan", value: "tajikistan", code: "+992" },
{ label: "Tanzania", value: "tanzania", code: "+255" },
{ label: "Thailand", value: "thailand", code: "+66" },
{ label: "TimorLeste", value: "timor-leste", code: "+670" },
{ label: "Togo", value: "togo", code: "+228" },
{ label: "Tonga", value: "tonga", code: "+676" },
{ label: "Trinidad and Tobago", value: "trinidad-and-tobago", code: "+1-868" },
{ label: "Tunisia", value: "tunisia", code: "+216" },
{ label: "Turkey", value: "turkey", code: "+90" },
{ label: "Turkmenistan", value: "turkmenistan", code: "+993" },
{ label: "Tuvalu", value: "tuvalu", code: "+688" },
{ label: "Uganda", value: "uganda", code: "+256" },
{ label: "Ukraine", value: "ukraine", code: "+380" },
{ label: "United Arab Emirates", value: "united-arab-emirates", code: "+971" },
{ label: "United Kingdom", value: "united-kingdom", code: "+44" },
{ label: "United States", value: "united-states", code: "+1" },
{ label: "Uruguay", value: "uruguay", code: "+598" },
{ label: "Uzbekistan", value: "uzbekistan", code: "+998" },
{ label: "Vanuatu", value: "vanuatu", code: "+678" },
{
label: "Vatican City",
value: "vatican-city",
code: "+379" /* برخی منابع مختلف است */,
},
{ label: "Venezuela", value: "venezuela", code: "+58" },
{ label: "Vietnam", value: "vietnam", code: "+84" },
{ label: "Yemen", value: "yemen", code: "+967" },
{ label: "Zambia", value: "zambia", code: "+260" },
{ label: "Zimbabwe", value: "zimbabwe", code: "+263" },
];

View File

@@ -0,0 +1,238 @@
// export const projectName="shomalhospital"
export const VALID_ORIGINS = [
/^http:\/\/localhost:\d+$/,
"https://ipd.shomalhospital.com",
"http://ipd.shomalhospital.com",
];
export const SELECT_STAFF_OUT_DATA = Object.freeze({
id: true,
username: true,
email: true,
is_verified: true,
role: true,
send_notif_with_email: true,
});
export const allCountries = [
{ label: "Afghanistan", value: "afghanistan", code: "+93" },
{ label: "Albania", value: "albania", code: "+355" },
{ label: "Algeria", value: "algeria", code: "+213" },
{ label: "American Samoa", value: "american-samoa", code: "+1-684" },
{ label: "Andorra", value: "andorra", code: "+376" },
{ label: "Angola", value: "angola", code: "+244" },
{ label: "Anguilla", value: "anguilla", code: "+1-264" },
{
label: "Antigua and Barbuda",
value: "antigua-and-barbuda",
code: "+1-268",
},
{ label: "Argentina", value: "argentina", code: "+54" },
{ label: "Armenia", value: "armenia", code: "+374" },
{ label: "Aruba", value: "aruba", code: "+297" },
{ label: "Australia", value: "australia", code: "+61" },
{ label: "Austria", value: "austria", code: "+43" },
{ label: "Azerbaijan", value: "azerbaijan", code: "+994" },
{ label: "Bahamas", value: "bahamas", code: "+1-242" },
{ label: "Bahrain", value: "bahrain", code: "+973" },
{ label: "Bangladesh", value: "bangladesh", code: "+880" },
{ label: "Barbados", value: "barbados", code: "+1-246" },
{ label: "Belarus", value: "belarus", code: "+375" },
{ label: "Belgium", value: "belgium", code: "+32" },
{ label: "Belize", value: "belize", code: "+501" },
{ label: "Benin", value: "benin", code: "+229" },
{ label: "Bhutan", value: "bhutan", code: "+975" },
{ label: "Bolivia", value: "bolivia", code: "+591" },
{
label: "Bosnia and Herzegovina",
value: "bosnia-and-herzegovina",
code: "+387",
},
{ label: "Botswana", value: "botswana", code: "+267" },
{ label: "Brazil", value: "brazil", code: "+55" },
{ label: "Brunei", value: "brunei", code: "+673" },
{ label: "Bulgaria", value: "bulgaria", code: "+359" },
{ label: "Burkina Faso", value: "burkina-faso", code: "+226" },
{ label: "Burundi", value: "burundi", code: "+257" },
{ label: "Cabo Verde", value: "cabo-verde", code: "+238" },
{ label: "Cambodia", value: "cambodia", code: "+855" },
{ label: "Cameroon", value: "cameroon", code: "+237" },
{ label: "Canada", value: "canada", code: "+1" },
{
label: "Central African Republic",
value: "central-african-republic",
code: "+236",
},
{ label: "Chad", value: "chad", code: "+235" },
{ label: "Chile", value: "chile", code: "+56" },
{ label: "China", value: "china", code: "+86" },
{ label: "Colombia", value: "colombia", code: "+57" },
{ label: "Comoros", value: "comoros", code: "+269" },
{ label: "Congo", value: "congo", code: "+242" },
{
label: "Congo, Democratic Republic of the",
value: "congo-democratic-republic",
code: "+243",
},
{ label: "Costa Rica", value: "costa-rica", code: "+506" },
{ label: "Croatia", value: "croatia", code: "+385" },
{ label: "Cuba", value: "cuba", code: "+53" },
{ label: "Cyprus", value: "cyprus", code: "+357" },
{ label: "Czech Republic", value: "czech-republic", code: "+420" },
{ label: "Denmark", value: "denmark", code: "+45" },
{ label: "Djibouti", value: "djibouti", code: "+253" },
{ label: "Dominica", value: "dominica", code: "+1-767" },
{ label: "Dominican Republic", value: "dominican-republic", code: "+1-809" },
{ label: "Ecuador", value: "ecuador", code: "+593" },
{ label: "Egypt", value: "egypt", code: "+20" },
{ label: "El Salvador", value: "el-salvador", code: "+503" },
{ label: "Equatorial Guinea", value: "equatorial-guinea", code: "+240" },
{ label: "Eritrea", value: "eritrea", code: "+291" },
{ label: "Estonia", value: "estonia", code: "+372" },
{ label: "Eswatini", value: "eswatini", code: "+268" },
{ label: "Ethiopia", value: "ethiopia", code: "+251" },
{ label: "Fiji", value: "fiji", code: "+679" },
{ label: "Finland", value: "finland", code: "+358" },
{ label: "France", value: "france", code: "+33" },
{ label: "Gabon", value: "gabon", code: "+241" },
{ label: "Gambia", value: "gambia", code: "+220" },
{ label: "Georgia", value: "georgia", code: "+995" },
{ label: "Germany", value: "germany", code: "+49" },
{ label: "Ghana", value: "ghana", code: "+233" },
{ label: "Greece", value: "greece", code: "+30" },
{ label: "Grenada", value: "grenada", code: "+1-473" },
{ label: "Guatemala", value: "guatemala", code: "+502" },
{ label: "Guinea", value: "guinea", code: "+224" },
{ label: "Guinea-Bissau", value: "guinea-bissau", code: "+245" },
{ label: "Guyana", value: "guyana", code: "+592" },
{ label: "Haiti", value: "haiti", code: "+509" },
{ label: "Honduras", value: "honduras", code: "+504" },
{ label: "Hungary", value: "hungary", code: "+36" },
{ label: "Iceland", value: "iceland", code: "+354" },
{ label: "India", value: "india", code: "+91" },
{ label: "Indonesia", value: "indonesia", code: "+62" },
{ label: "Iran", value: "iran", code: "+98" },
{ label: "Iraq", value: "iraq", code: "+964" },
{ label: "Ireland", value: "ireland", code: "+353" },
{ label: "Israel", value: "israel", code: "+972" },
{ label: "Italy", value: "italy", code: "+39" },
{ label: "Jamaica", value: "jamaica", code: "+1-876" },
{ label: "Japan", value: "japan", code: "+81" },
{ label: "Jordan", value: "jordan", code: "+962" },
{ label: "Kazakhstan", value: "kazakhstan", code: "+7" },
{ label: "Kenya", value: "kenya", code: "+254" },
{ label: "Kiribati", value: "kiribati", code: "+686" },
{ label: "Kuwait", value: "kuwait", code: "+965" },
{ label: "Kyrgyzstan", value: "kyrgyzstan", code: "+996" },
{ label: "Laos", value: "laos", code: "+856" },
{ label: "Latvia", value: "latvia", code: "+371" },
{ label: "Lebanon", value: "lebanon", code: "+961" },
{ label: "Lesotho", value: "lesotho", code: "+266" },
{ label: "Liberia", value: "liberia", code: "+231" },
{ label: "Libya", value: "libya", code: "+218" },
{ label: "Liechtenstein", value: "liechtenstein", code: "+423" },
{ label: "Lithuania", value: "lithuania", code: "+370" },
{ label: "Luxembourg", value: "luxembourg", code: "+352" },
{ label: "Macau", value: "macau", code: "+853" },
{ label: "North Macedonia", value: "north-macedonia", code: "+389" },
{ label: "Madagascar", value: "madagascar", code: "+261" },
{ label: "Malawi", value: "malawi", code: "+265" },
{ label: "Malaysia", value: "malaysia", code: "+60" },
{ label: "Maldives", value: "maldives", code: "+960" },
{ label: "Mali", value: "mali", code: "+223" },
{ label: "Malta", value: "malta", code: "+356" },
{ label: "Marshall Islands", value: "marshall-islands", code: "+692" },
{ label: "Mauritania", value: "mauritania", code: "+222" },
{ label: "Mauritius", value: "mauritius", code: "+230" },
{ label: "Mexico", value: "mexico", code: "+52" },
{ label: "Micronesia", value: "micronesia", code: "+691" },
{ label: "Moldova", value: "moldova", code: "+373" },
{ label: "Monaco", value: "monaco", code: "+377" },
{ label: "Mongolia", value: "mongolia", code: "+976" },
{ label: "Montenegro", value: "montenegro", code: "+382" },
{ label: "Morocco", value: "morocco", code: "+212" },
{ label: "Mozambique", value: "mozambique", code: "+258" },
{ label: "Myanmar", value: "myanmar", code: "+95" },
{ label: "Namibia", value: "namibia", code: "+264" },
{ label: "Nauru", value: "nauru", code: "+674" },
{ label: "Nepal", value: "nepal", code: "+977" },
{ label: "Netherlands", value: "netherlands", code: "+31" },
{ label: "NewZealand", value: "new-zealand", code: "+64" },
{ label: "Nicaragua", value: "nicaragua", code: "+505" },
{ label: "Niger", value: "niger", code: "+227" },
{ label: "Nigeria", value: "nigeria", code: "+234" },
{ label: "Norway", value: "norway", code: "+47" },
{ label: "Oman", value: "oman", code: "+968" },
{ label: "Pakistan", value: "pakistan", code: "+92" },
{ label: "Palau", value: "palau", code: "+680" },
{ label: "Palestine", value: "palestine", code: "+970" },
{ label: "Panama", value: "panama", code: "+507" },
{ label: "Papua New Guinea", value: "papua-new-guinea", code: "+675" },
{ label: "Paraguay", value: "paraguay", code: "+595" },
{ label: "Peru", value: "peru", code: "+51" },
{ label: "Philippines", value: "philippines", code: "+63" },
{ label: "Poland", value: "poland", code: "+48" },
{ label: "Portugal", value: "portugal", code: "+351" },
{ label: "Qatar", value: "qatar", code: "+974" },
{ label: "Romania", value: "romania", code: "+40" },
{ label: "Russia", value: "russia", code: "+7" },
{ label: "Rwanda", value: "rwanda", code: "+250" },
{ label: "Samoa", value: "samoa", code: "+685" },
{ label: "San Marino", value: "san-marino", code: "+378" },
{ label: "Saudi Arabia", value: "saudi-arabia", code: "+966" },
{ label: "Senegal", value: "senegal", code: "+221" },
{ label: "Serbia", value: "serbia", code: "+381" },
{ label: "Seychelles", value: "seychelles", code: "+248" },
{ label: "Sierra Leone", value: "sierra-leone", code: "+232" },
{ label: "Singapore", value: "singapore", code: "+65" },
{ label: "Slovakia", value: "slovakia", code: "+421" },
{ label: "Slovenia", value: "slovenia", code: "+386" },
{ label: "Solomon Islands", value: "solomon-islands", code: "+677" },
{ label: "Somalia", value: "somalia", code: "+252" },
{ label: "South Africa", value: "south-africa", code: "+27" },
{ label: "South Sudan", value: "south-sudan", code: "+211" },
{ label: "Spain", value: "spain", code: "+34" },
{ label: "Sri Lanka", value: "sri-lanka", code: "+94" },
{ label: "Sudan", value: "sudan", code: "+249" },
{ label: "Suriname", value: "suriname", code: "+597" },
{ label: "Sweden", value: "sweden", code: "+46" },
{ label: "Switzerland", value: "switzerland", code: "+41" },
{ label: "Syria", value: "syria", code: "+963" },
{ label: "Taiwan", value: "taiwan", code: "+886" },
{ label: "Tajikistan", value: "tajikistan", code: "+992" },
{ label: "Tanzania", value: "tanzania", code: "+255" },
{ label: "Thailand", value: "thailand", code: "+66" },
{ label: "TimorLeste", value: "timor-leste", code: "+670" },
{ label: "Togo", value: "togo", code: "+228" },
{ label: "Tonga", value: "tonga", code: "+676" },
{
label: "Trinidad and Tobago",
value: "trinidad-and-tobago",
code: "+1-868",
},
{ label: "Tunisia", value: "tunisia", code: "+216" },
{ label: "Turkey", value: "turkey", code: "+90" },
{ label: "Turkmenistan", value: "turkmenistan", code: "+993" },
{ label: "Tuvalu", value: "tuvalu", code: "+688" },
{ label: "Uganda", value: "uganda", code: "+256" },
{ label: "Ukraine", value: "ukraine", code: "+380" },
{
label: "United Arab Emirates",
value: "united-arab-emirates",
code: "+971",
},
{ label: "United Kingdom", value: "united-kingdom", code: "+44" },
{ label: "United States", value: "united-states", code: "+1" },
{ label: "Uruguay", value: "uruguay", code: "+598" },
{ label: "Uzbekistan", value: "uzbekistan", code: "+998" },
{ label: "Vanuatu", value: "vanuatu", code: "+678" },
{
label: "Vatican City",
value: "vatican-city",
code: "+379" /* برخی منابع مختلف است */,
},
{ label: "Venezuela", value: "venezuela", code: "+58" },
{ label: "Vietnam", value: "vietnam", code: "+84" },
{ label: "Yemen", value: "yemen", code: "+967" },
{ label: "Zambia", value: "zambia", code: "+260" },
{ label: "Zimbabwe", value: "zimbabwe", code: "+263" },
];

10
src/common/lib/prisma.js Normal file
View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.prisma = void 0;
require("dotenv/config");
var adapter_pg_1 = require("@prisma/adapter-pg");
var client_1 = require("../../generated/prisma/client");
var connectionString = "".concat(process.env.DATABASE_URL);
var adapter = new adapter_pg_1.PrismaPg({ connectionString: connectionString });
var prisma = new client_1.PrismaClient({ adapter: adapter });
exports.prisma = prisma;

10
src/common/lib/prisma.ts Normal file
View File

@@ -0,0 +1,10 @@
import "dotenv/config";
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from "../../generated/prisma/client";
const connectionString = `${process.env.DATABASE_URL}`
const adapter = new PrismaPg({ connectionString })
const prisma = new PrismaClient({ adapter })
export { prisma }

32
src/common/types/index.ts Normal file
View File

@@ -0,0 +1,32 @@
import {StaffRoles} from "@/generated/prisma/enums";
import {Response} from "express";
export interface ServerResponseObject {
status: number;
data: any;
message?: string;
}
export interface ServerErrorsObject {
status: number;
data?: any;
error: {
message: string;
description?: string;
};
}
export type ServerResponse = Response<
ServerResponseObject | ServerErrorsObject
>;
export const CaseStatusConst = {
NEW: "NEW",
CONTACTED: "CONTACTED",
DOCS_PENDING: "DOCS_PENDING",
REVIEWING: "REVIEWING",
PRE_APPROVED: "PRE_APPROVED",
REJECTED: "REJECTED",
CLOSED: "CLOSED",
CONVERTED_TO_HIS: "CONVERTED_TO_HIS",
} as const;

View File

@@ -0,0 +1,71 @@
import Excel from "exceljs";
import axios from "axios";
import FormData = require("form-data");
function cleanRecord(record: any) {
const newRecord: any = {};
for (const key in record) {
const value = record[key];
if (Array.isArray(value)) {
// اگر آرایه از آبجکت‌هاست، مقادیر مهم را ترکیب کن
newRecord[key] = value
.map((item) =>
item.firstName && item.lastName && item.position
? `${item.firstName} ${item.lastName} (${item.position})`
: JSON.stringify(item)
)
.join(", ");
} else if (typeof value === "object" && value !== null) {
// اگر آبجکت است، مثلا translations
if (value.translations) {
newRecord[key] = value.translations
.map((t: any) => t.displayName)
.join(", ");
} else {
newRecord[key] = JSON.stringify(value);
}
} else {
newRecord[key] = value;
}
}
return newRecord;
}
export async function exportToExcel(
records: Record<string, any>[],
columns: { header: string; key: string; width?: number }[],
fileName: string = "export"
) {
if (!records.length) throw new Error("داده‌ای برای خروجی وجود ندارد");
const wb = new Excel.Workbook();
const ws = wb.addWorksheet("Export");
ws.columns = columns;
ws.addRows(records); // داده‌ها را اضافه می‌کنیم
const excelBuffer = await wb.xlsx.writeBuffer();
// ارسال به CDN یا ذخیره محلی
const form = new FormData();
form.append("exports", excelBuffer, {
filename: `${fileName}-${Date.now()}.xlsx`,
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const cdnRes = await axios.post(process.env.CDN_UPLOAD_EXPORTS_URL!, form, {
headers: {
...form.getHeaders(),
Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN}`,
},
maxBodyLength: Infinity,
maxContentLength: Infinity,
});
return cdnRes.data?.url || null;
}

View File

@@ -0,0 +1,290 @@
import createHttpError from "http-errors";
import {ServerResponse} from "../types";
import {Prisma, PrismaClient} from "@/generated/prisma/client";
import {prisma} from "../lib/prisma";
import {createUserTranslationsType} from "@/modules/users/types";
import {createSlug} from "./generate";
const crypto = require("crypto");
const algorithm = "aes-256-cbc";
const key = Buffer.from(process.env.ENCRYPTION_KEY!);
const nodemailer = require("nodemailer");
interface SlugTranslation {
lang_id: number;
langSlug?: string; // مثلا fa, en (اختیاری ولی ترجیحی)
[key: string]: string | number | null | undefined;
}
interface GenerateSlugOptions {
preferredLangSlug?: string; // پیش‌فرض: 'fa'
fieldsPriority: string[]; // مثلا ['firstName', 'lastName']
fallback?: string; // اگر همه‌چی خالی بود
}
export async function sendEmail({
to,
subject,
text,
}: {
to: string;
subject: string;
text: string;
}) {
const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: true,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
tls: {
rejectUnauthorized: false, // اگر SSL سرور قدیمی بود مشکل حل می‌کند
},
});
await transporter.sendMail({
from: `"${process.env.PROJECT_NAME}" <no-reply@shomalhospital.com>`,
to,
subject,
text,
});
}
export async function id_validation(id: any) {
if (!id) {
throw new createHttpError.BadRequest("درخواست نامعتبر است");
}
// else if (isNaN(id)) {
// throw new createHttpError.BadRequest("درخواست نامعتبر است");
// }
return true;
}
export function maskMobile(mobile: string) {
// اطمینان از اینکه شماره فقط ارقام هست
const digits = mobile.replace(/\D/g, "");
// بررسی فرمت شماره ایران (11 رقمی، شروع با 09)
if (!/^09\d{9}$/.test(digits)) {
throw new Error("شماره موبایل معتبر ایران نیست");
}
// ماسک‌کردن سه رقم وسط
return digits.replace(/^(\d{4})(\d{3})(\d{4})$/, "$1***$3");
}
export function addMinutes(date: Date, minutes: number) {
return new Date(date.getTime() + minutes * 60 * 1000);
}
export function handlePrismaError(err: any) {
if (err instanceof Prisma.PrismaClientKnownRequestError) {
console.log(err)
switch (err.code) {
case "P2002": {
const target = err.meta?.target;
const fields = Array.isArray(target)
? target.join(", ")
: target ?? "field";
throw new createHttpError.Conflict(`این دیتا قبلا ثبت شده است`);
}
case "P2025":
throw new createHttpError.NotFound("پیدا نشد");
case "P2003":
throw new createHttpError.InternalServerError(
"امکان انجام این عملیات وجود ندارد"
);
default:
throw new createHttpError.InternalServerError("خطایی رخ داده است");
}
}
throw new createHttpError.InternalServerError("خطایی رخ داده است");
}
export function buildPaginationResult<T>(
page: number,
limit: number,
total: number,
data: T[]
) {
return {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
data,
};
}
interface PaginationOptions {
include?: Record<string, any>;
where?: Record<string, any>;
orderBy?: Record<string, any>;
}
export async function buildPagination(
req: any,
res: ServerResponse,
table: keyof PrismaClient,
options: PaginationOptions = {}
) {
const page = Number(req?.query?.page ?? 1);
const limit = Number(req?.query?.limit ?? 10);
const skip = (page - 1) * limit;
const model = prisma[table] as any;
if (!model?.findMany) {
return res.status(400).json({
status: 400,
error: {message: `Model '${table.toString()}' is not queryable.`},
});
}
const {include, where, orderBy} = options;
const [data, total] = await Promise.all([
model.findMany({
skip,
take: limit,
where,
include,
orderBy: orderBy ?? {createdAt: "desc"},
}),
model.count({where}),
]);
return buildPaginationResult(page, limit, total, data);
}
export function mapUserTranslationsToPrismaCreate(
translations: createUserTranslationsType[]
) {
return translations.map((tr) => ({
lang: {
connect: {
id: tr.lang_id,
},
},
firstName: tr.firstName,
lastName: tr.lastName,
position: tr.position,
}));
}
export async function generateSlugFromTranslations(
translations: SlugTranslation[],
{preferredLangSlug = "fa", fieldsPriority, fallback = ""}: GenerateSlugOptions
) {
if (!translations?.length) return fallback;
// 1. انتخاب ترجمه مرجع
let main =
translations.find((t) => t.langSlug === preferredLangSlug) ??
translations.find((t) => t.langSlug === "en") ??
translations[0];
// 2. استخراج فیلدهای معتبر
const parts = fieldsPriority
.map((field) => main[field])
.filter((v): v is string => typeof v === "string" && v.trim().length > 0);
if (!parts.length) return fallback;
// 3. ساخت slug
return await createSlug(parts.join(" "));
}
export function mapDbTranslationsForSlug(
translations: Array<{
lang_id: number | null;
lang?: {slug: string} | null;
[key: string]: any;
}>
) {
return translations.map((t) => ({
...t,
lang_id: t.lang_id!,
langSlug: t.lang?.slug,
}));
}
export function encrypt(text: string) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
return iv.toString("hex") + ":" + encrypted;
}
export function decrypt(text: string) {
const [ivHex, encrypted] = text.split(":");
const iv = Buffer.from(ivHex, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
function luhnCheckDigit(input: string): number {
let sum = 0;
let shouldDouble = true;
for (let i = input.length - 1; i >= 0; i--) {
let digit = parseInt(input[i], 10);
if (shouldDouble) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
shouldDouble = !shouldDouble;
}
return (10 - (sum % 10)) % 10;
}
export async function generatePatientPID(): Promise<string> {
const now = new Date();
const year = String(now.getFullYear()).slice(2); // YY
const month = String(now.getMonth() + 1).padStart(2, "0"); // MM
const prefix = `${year}${month}`; // YYMM
const lastPatient = await prisma.patient.findFirst({
where: {
pid: {
startsWith: prefix,
},
},
orderBy: {
pid: "desc",
},
select: {
pid: true,
},
});
let nextSeq = 1;
if (lastPatient) {
nextSeq = parseInt(lastPatient.pid.slice(4, 9), 10) + 1;
}
const sequence = String(nextSeq).padStart(5, "0");
const base = `${prefix}${sequence}`; // 9 رقم
const checkDigit = luhnCheckDigit(base);
return `${base}${checkDigit}`; // 10 رقم
}

View File

@@ -0,0 +1,178 @@
import crypto from "crypto";
import bcrypt from "bcrypt";
import jwt, {Secret, SignOptions} from "jsonwebtoken";
import dotenv from "dotenv";
import {token_policy} from "../constants/policy";
import slugify from "slugify";
dotenv.config();
export const HashPassword = async (password: string): Promise<string> => {
const saltRounds = 10;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
} catch (error) {
throw new Error("hashing Error");
}
};
export async function comparePassword(password: string, hash: string) {
return await bcrypt.compare(password, hash);
}
export async function generateSecureToken(
length: number = 32,
encoding: BufferEncoding = "hex"
): Promise<string> {
return await crypto.randomBytes(length).toString(encoding);
}
// token
export interface jwt_token_payload {
id: string;
role?: string;
}
const secretKey: Secret =
process.env.JWT_SECRET || crypto.randomBytes(64).toString("hex");
const RefreshSecretKey: Secret =
process.env.JWT_SECRET || crypto.randomBytes(64).toString("hex");
export async function generateAccessToken(
payload: jwt_token_payload,
expiresIn: `${number}${
| "s"
| "m"
| "h"
| "d"
| "y"}` = token_policy.a_token_expire
): Promise<string> {
const options: SignOptions = {expiresIn};
return new Promise((resolve, reject) => {
jwt.sign(payload, secretKey, options, (err, token) => {
if (err || !token) {
return reject(err || new Error("Token generation failed"));
}
resolve(token);
});
});
}
export async function generateRefreshToken(
payload: jwt_token_payload,
rememberMe: boolean
): Promise<string> {
return new Promise((resolve, reject) => {
jwt.sign(
payload,
RefreshSecretKey,
{expiresIn: rememberMe ? "30d" : token_policy.r_token_expire},
(err, token) => {
if (err || !token) {
return reject(err || new Error("Refresh Token generation failed"));
}
resolve(token);
}
);
});
}
export async function verifyAccessToken(token: string) {
return new Promise((resolve, reject) => {
jwt.verify(token, secretKey, (err, result) => {
if (err) {
return reject(new Error("verify access token failed"));
}
resolve(result);
});
});
}
export async function verifyRefreshToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(token, RefreshSecretKey, (err, result) => {
if (err) {
return reject(new Error("verify refresh token failed"));
}
resolve(result);
});
});
}
export async function generateSlug(text: string): Promise<string> {
return await text
.toLowerCase()
.trim()
.replace(/[\u064B-\u0652]/g, "") // حذف حرکات عربی
.replace(/[^\w\u0600-\u06FF\s-]/g, "") // حذف کاراکترهای غیرمجاز (غیر از حروف، اعداد، خط تیره و فاصله)
.replace(/[\s_-]+/g, "-") // تبدیل فاصله‌ها و خط زیر به خط تیره
.replace(/^-+|-+$/g, ""); // حذف خط‌تیره‌های اضافی اول و آخر
}
export interface UploadTokenPayload {
userId: string;
type: "document" | "image";
mime: string;
maxSize: number;
exp: number;
nonce: string;
}
/**
* تولید توکن یک‌بار مصرف برای آپلود
*/
export function generateUploadToken(
payload: Omit<UploadTokenPayload, "exp" | "nonce">
) {
const fullPayload: UploadTokenPayload = {
...payload,
exp: Date.now() + 30_000, // 30 ثانیه اعتبار
nonce: crypto.randomBytes(16).toString("hex"), // یک‌بار مصرف
};
const payloadString = JSON.stringify(fullPayload);
const signature = crypto
.createHmac("sha256", process.env.UPLOAD_SECRET!)
.update(payloadString)
.digest("hex");
return Buffer.from(
JSON.stringify({payload: fullPayload, signature})
).toString("base64");
}
/**
* اعتبارسنجی توکن یک‌بار مصرف
*/
export function verifyUploadToken(token: string): UploadTokenPayload {
const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf-8"));
const {payload, signature} = decoded;
const expectedSig = crypto
.createHmac("sha256", process.env.UPLOAD_SECRET!)
.update(JSON.stringify(payload))
.digest("hex");
if (expectedSig !== signature) throw new Error("Invalid token");
if (payload.exp < Date.now()) throw new Error("Token expired");
return payload;
}
export function generateIPDReceptionCode() {
const randomNumber = Math.floor(100000 + Math.random() * 900000); // عدد 6 رقمی
return `ipd-r-${randomNumber}`;
}
export function generateIPDConsultantCode() {
const randomNumber = Math.floor(100000 + Math.random() * 900000); // عدد 6 رقمی
return `ipd-c-${randomNumber}`;
}
export async function createSlug(input: string) {
// const latin = transliterate(input); // تبدیل از فارسی/عربی → لاتین
return await slugify(input, {
lower: true,
strict: true,
trim: true,
});
}

View File

@@ -0,0 +1,17 @@
import {prisma} from "../lib/prisma";
export async function createAuditLog(data: any) {
return prisma.auditLog.create({data});
}
export async function createAccessLog(data: any) {
return prisma.accessLog.create({data});
}
export async function createDecisionLog(data: any) {
return prisma.decisionLog.create({data});
}
export async function createPolicyAcceptanceLog(data: any) {
return prisma.tosAcceptanceLog.create({data});
}

View File

@@ -0,0 +1,97 @@
import {prisma} from "@/common/lib/prisma";
import {CaseStatus} from "@/generated/prisma/enums";
import createHttpError from "http-errors";
const autoBind = require("auto-bind");
export class Controller {
constructor() {
autoBind(this);
}
async updateCaseStatus(
caseId: string,
newStatus: CaseStatus,
staffId?: string
) {
// وضعیت فعلی را بگیر
const existing = await prisma.onlineCase.findUnique({
where: {id: caseId},
select: {status: true},
});
if (!existing) {
throw new Error("Case Not Found");
}
// اگر تغییری نکرده بود، بی‌خود تاریخچه نساز
if (existing.status === newStatus) {
return;
}
// تغییر وضعیت Case
await prisma.onlineCase.update({
where: {id: caseId},
data: {status: newStatus},
});
// ساخت رکورد تاریخچه
await prisma.caseStatusHistory.create({
data: {
caseId: caseId,
from: existing.status,
to: newStatus,
changedBy: staffId || null,
},
});
return true;
}
async isPatientExist(id: string) {
const patient = await prisma.patient.findUnique({
where: {id},
include: {
cases: {
select: {
status: true,
},
},
},
});
if (!patient) {
throw new createHttpError.NotFound("بیمار یافت نشد");
}
return patient;
}
async isOnlineCaseExist(id: string) {
const OnlineCase = await prisma.onlineCase.findUnique({
where: {id},
});
if (!OnlineCase) {
throw new createHttpError.NotFound("پرونده بیمار یافت نشد");
}
return OnlineCase;
}
async isExistLanguage(id: number) {
const language = await prisma.language.findUnique({where: {id}});
if (!language) {
return false;
}
return language;
}
async findLanguageBySlug(slug:string) {
const language = await prisma.language.findUnique({where: {slug}});
if (!language) {
return false;
}
return language;
}
}

20
src/core/getconfig.ts Normal file
View File

@@ -0,0 +1,20 @@
import {prisma} from "@/common/lib/prisma";
export async function getConfig(key: string) {
const item = await prisma.panelConfig.findUnique({where: {key}});
if (!item) return null;
switch (item.type) {
case "BOOLEAN":
return item.value === "true";
case "NUMBER":
return Number(item.value);
case "JSON":
return JSON.parse(item.value);
case "STRING_ARRAY":
return item.value;
default:
return item.value;
}
}

View File

@@ -0,0 +1,66 @@
import {NextFunction} from "express";
import {HttpStatusCode} from "axios";
import {ServerResponse} from "@/common/types";
import {jwt_token_payload, verifyAccessToken} from "@/common/utils/generate";
import {prisma} from "@/common/lib/prisma";
import {handlePrismaError} from "@/common/utils/functions";
export const authMiddleware = async (
req: any,
res: ServerResponse,
next: NextFunction
) => {
const token = req.signedCookies?.accessToken;
if (!token || typeof token !== "string")
return res.status(HttpStatusCode.Forbidden).json({
status: HttpStatusCode.Forbidden,
error: {
message: "دسترسی شما غیرمجاز است",
description: "denied",
},
});
try {
const decoded = (await verifyAccessToken(token)) as jwt_token_payload;
try {
const user = await prisma.staff.findUnique({
where: {
id: decoded.id,
},
});
req.user = user;
} catch (error) {
handlePrismaError(error);
}
next();
} catch {
next(new Error("دسترسی شما منقضی شده و یا نامعتبر است"));
}
};
export const optionalAuthMiddleware = async (
req: any,
res: ServerResponse,
next: NextFunction
) => {
const token = req.signedCookies?.accessToken;
if (!token) {
return next();
}
if (token && typeof token !== "string")
return res.status(HttpStatusCode.Unauthorized).json({
status: HttpStatusCode.Unauthorized,
error: {
message: "دسترسی شما غیرمجاز است",
description: "unauthorized",
},
});
try {
const decoded = await verifyAccessToken(token);
req.user = decoded;
return next();
} catch {}
return next();
};

View File

@@ -0,0 +1,43 @@
import {prisma} from "@/common/lib/prisma";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
export async function checkLanguageSlug(
req: any,
res: ServerResponse,
next: NextFunction
) {
try {
const langSlug = req.params?.lang;
if (!langSlug) {
return res.status(400).json({
status: 400,
error: {
message: "Language slug is required",
},
});
}
const language = await prisma.language.findUnique({
where: {slug: langSlug},
select: {id: true},
});
if (!language) {
return res.status(404).json({
status: 404,
error: {
message: "Language not found",
},
});
}
// اگر خواستی بعداً استفاده کنی
req.langId = language.id;
next();
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,48 @@
import {ServerResponse} from "@/common/types";
import {Staff} from "@/generated/prisma/client";
import {NextFunction} from "express";
export async function tosGuard(
req: any,
res: ServerResponse,
next: NextFunction
) {
const user = req.user as Staff;
if (!user) {
return res
.status(401)
.json({status: 401, error: {message: "User not authenticated"}});
}
// banned
if (user.status === "BANNED") {
return res.status(403).json({
status: 403,
error: {
message: "Account banned",
description: "Violation of terms or strikes exceeded",
},
});
}
// restricted (مثلاً وقتی strikes >= 3)
if (user.status === "RESTRICTED") {
return res.status(429).json({
status: 429,
data: {
strikes: user.strikes,
trustScore: user.trustScore,
},
error: {message: "Account temporarily restricted"},
});
}
// warned (اختیاری می‌تونی alert بدی)
if (user.status === "WARNED") {
req.user.isWarned = true; // می‌تونی جلوی UI هشدار بدی
}
// همه چی اوکی
next();
}

View File

@@ -0,0 +1,36 @@
import {ServerResponse} from "@/common/types";
import {getConfig} from "../getconfig";
import {NextFunction} from "express";
export function configCheck(configKeys: string[]) {
return async function (req: any, res: ServerResponse, next: NextFunction) {
try {
for (const key of configKeys) {
const value = await getConfig(key);
// اگر اصلاً کانفیگ وجود نداشت یا false بود
if (!value) {
return res.status(503).json({
status: 503,
data: {
success: false,
config: key,
},
message: `ماژول «${key}» غیرفعال است`,
});
}
}
// همه OK بودند → ادامه بده
next();
} catch (error) {
return res.status(500).json({
status: 500,
data: {
success: false,
},
message: "خطا در بررسی کانفیگ‌ها",
});
}
};
}

View File

@@ -0,0 +1,36 @@
import { ServerResponse } from "@/common/types";
import { createAccessLog } from "@/common/utils/logger";
import { NextFunction } from "express";
import { ApiRequestLog } from "mongodb/models";
export function apiLoggingMiddleware() {
return async (req: any & { user?: { id: number; role: string } }, res: ServerResponse, next: NextFunction) => {
const start = Date.now();
res.on("finish", async () => {
// Mongo async (حجم بالا، غیر حساس)
ApiRequestLog.create({
method: req.method,
path: req.path,
status: res.statusCode,
durationMs: Date.now() - start,
userId: req.user?.id,
role: req.user?.role,
}).catch(console.error);
// Prisma async (حساس، حقوقی)
if (req.user) {
createAccessLog({
userId: req.user.id,
userRole: req.user.role,
resource: req.path,
resourceId: req.params.id ? Number(req.params.id) : null,
reason: "ACCESS_CHECK",
ip: req.ip,
}).catch(console.error);
}
});
next();
};
}

View File

@@ -0,0 +1,76 @@
// async function getPermissionsForRole(roleId: number): Promise<Set<string>> {
// const role = db.role.findUnique({
// where: { id: roleId },
// include: {
// policies: { include: { policy: true } },
// policy_sets: {
// include: {
// set: { include: { policies: true } }
// }
// }
// }
// });
import { prisma } from "@/common/lib/prisma";
import { ServerResponse } from "@/common/types";
import { Staff } from "@/generated/prisma/client";
import { StaffRoles } from "@/generated/prisma/enums";
import { NextFunction } from "express";
import createHttpError from "http-errors";
// if (!role) return new Set();
// const directPermissions = Object.values(role.policies).map(rp => rp.policy.action);
// const setPermissions = Object.values(role.policy_sets).flatMap(rps => Object.values(rps.set.policies).map((p:any) => p.action));
// return new Set([...directPermissions, ...setPermissions]);
// }
// // میدل‌ویر چک مجوز خاص
// export function role_authorize(action: string) {
// return async function (req: any, res: Response, next: NextFunction) {
// try {
// // فرض: کاربر تو req.user ذخیره شده و roleId داره
// const user = req.user as { id: number; roleId: number } | undefined;
// if (!user) {
// return res.status(401).json({ message: 'Unauthorized: User not logged in' });
// }
// const permissions = await getPermissionsForRole(user.roleId);
// if (!permissions.has(action)) {
// return res.status(403).json({ message: 'Forbidden: Access denied' });
// }
// next();
// } catch (error) {
// res.status(500).json({ message: 'Internal server error' });
// }
// };
// }
export function role_authorize(...allowed_roles: StaffRoles[]) {
return async (req: any, res: ServerResponse, next: NextFunction) => {
const user_data = req.user as Staff;
if (!user_data.id) {
throw new createHttpError.Unauthorized("حساب کاربری شما نامعتبر است");
}
if (user_data?.role && !allowed_roles.includes(user_data?.role)) {
return res
.status(403)
.json({
status: 403,
error: {message: "دسترسی شما به این بخش غیرمجاز است"},
});
}
next();
};
}

View File

@@ -0,0 +1,20 @@
import {ServerResponse} from "@/common/types";
import {Staff} from "@/generated/prisma/client";
import {NextFunction} from "express";
import createHttpError from "http-errors";
export async function checkVerify(
req: any,
res: ServerResponse,
next: NextFunction
) {
const user = req.user as Staff;
if (!user.is_verified) {
throw new createHttpError.Unauthorized(
"حساب کاربری شما تایید نشده است ، لطفا با مدیر سیستم تماس بگیرید"
);
}
next();
}

6
src/core/policies.ts Normal file
View File

@@ -0,0 +1,6 @@
export const policies = {
SPAM: {severity: 10, strike: 1},
RATE_LIMIT: {severity: 5, strike: 1},
CONTENT: {severity: 20, strike: 2},
ABUSE: {severity: 50, strike: 3},
};

View File

@@ -0,0 +1,41 @@
import auth_router from "@/modules/auth/routes/index.router";
import onlineCase_router from "@/modules/online-case/router";
import patients_router from "@/modules/patient/router/index.router";
import review_router from "@/modules/review/router";
import staff_router from "@/modules/staff/router/index.router";
import statistics_router from "@/modules/statistics/index.router";
import upload_router from "@/modules/upload/routes/index.router";
import users_router from "@/modules/users/router/index.router";
import router from "express";
import expertise_router from "@/modules/expertise/router/index.router";
import language_router from "@/modules/language/router";
import country_router from "@/modules/country/router";
import default_info_router from "@/modules/default/router/index.router";
import configs_router from "@/modules/configs/router/index.router";
import tos_router from "@/modules/tos/router";
import privacy_policy_router from "@/modules/privacy-policy/router";
import medical_package_router from "@/modules/medical-packages/router/index.router";
import transfer_team_router from "@/modules/transfer-team/router";
import publicApisRouter from "@/modules/public-apis/index.router";
const mainRouter = router.Router();
mainRouter.use("/auth", auth_router);
mainRouter.use("/user", users_router);
mainRouter.use("/staff", staff_router);
mainRouter.use("/upload", upload_router);
mainRouter.use("/patient", patients_router);
mainRouter.use("/case", onlineCase_router);
mainRouter.use("/review", review_router);
mainRouter.use("/statistics", statistics_router);
mainRouter.use('/expertise', expertise_router)
mainRouter.use('/language',language_router)
mainRouter.use('/country',country_router)
mainRouter.use('/configs',configs_router)
mainRouter.use('/default-info',default_info_router)
mainRouter.use('/tos',tos_router)
mainRouter.use('/pp',privacy_policy_router)
mainRouter.use('/medical-packages',medical_package_router)
mainRouter.use('/transfer-team',transfer_team_router)
mainRouter.use('/public-apis',publicApisRouter)
export default mainRouter;

29
src/core/secure-app.ts Normal file
View File

@@ -0,0 +1,29 @@
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import cors from "cors";
import cookieParser from "cookie-parser";
import csurf from "csurf";
import hpp from "hpp";
import { cors_option, helmet_option, limiter_option } from "@/common/constants/config";
export const rateLimiter = rateLimit(limiter_option);
export const corsOptions = cors(cors_option);
export const securityHeaders = helmet(helmet_option);
export const csrfProtection = [
cookieParser(),
csurf({cookie: {httpOnly: true, secure: true, sameSite: "strict"}}),
];
export const sanitizeData = [
hpp(),
];
export const secureApp = [
corsOptions,
rateLimiter,
securityHeaders,
...sanitizeData,
];

View File

@@ -0,0 +1,47 @@
import { prisma } from "@/common/lib/prisma";
import { policies } from "./policies";
export async function registerViolation(
userId: string,
type: keyof typeof policies,
reason: string
) {
const policy = policies[type];
const user = await prisma.staff.findUnique({where: {id: userId}});
if (!user) throw new Error("User not found");
// ثبت Violation
await prisma.violation.create({
data: {
userId,
type,
severity: policy.severity,
reason,
},
});
// به‌روزرسانی Strikes و TrustScore
const newStrikes = user.strikes + policy.strike;
const newTrustScore = user.trustScore - policy.severity;
let newStatus: typeof user.status = user.status;
if (newTrustScore <= 0 || newStrikes >= 5) {
newStatus = "BANNED";
} else if (newStrikes >= 3) {
newStatus = "RESTRICTED";
} else if (newStrikes >= 1) {
newStatus = "WARNED";
}
await prisma.staff.update({
where: {id: userId},
data: {
strikes: newStrikes,
trustScore: newTrustScore,
status: newStatus,
},
});
}

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import ServerApplication from "./app/server";
new ServerApplication()

View File

@@ -0,0 +1,150 @@
import {Controller} from "@/core/controller/main.controller";
import {NextFunction, Request} from "express";
import AuthService from "../service/AuthService";
import {
ResetPasswordRequestValidationSchema,
SigninFormDataValidationSchema,
} from "../validation";
import {AUTH_MESSAGES} from "../messages";
import {ServerResponse} from "@/common/types";
import {getConfig} from "@/core/getconfig";
import {token_policy} from "@/common/constants/policy";
class AuthControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = AuthService;
}
async login(req: Request, res: ServerResponse, next: NextFunction) {
try {
await SigninFormDataValidationSchema.validateAsync(req.body ?? {}, {
abortEarly: true,
stripUnknown: true,
});
const {username, password, rememberMe} = req.body;
const {accessToken, refreshToken} = await this.#service.login(
username,
password,
rememberMe ?? false
);
if (rememberMe) {
return res
.cookie("accessToken", accessToken, token_policy.access_token_options)
.cookie(
"refreshToken",
refreshToken,
token_policy.refresh_token_options
)
.json({
status: 200,
data: {},
message: AUTH_MESSAGES.success[200],
});
} else {
return res
.cookie("accessToken", accessToken, token_policy.access_token_options)
.json({
status: 200,
data: {},
message: AUTH_MESSAGES.success[200],
});
}
} catch (error) {
console.log(error);
next(error);
}
}
async forgotPassword(req: any, res: ServerResponse, next: NextFunction) {
const emailEnabled = await getConfig("email.enabled");
if (!emailEnabled) {
return res.status(500).json({
status: 500,
error: {message: "امکان ارسال ایمیل غیرفعال است"},
});
}
try {
await ResetPasswordRequestValidationSchema.validateAsync(req.body ?? {}, {
abortEarly: true,
stripUnknown: true,
});
const {username, email} = req.body;
await this.#service.forgot_password(username, email);
return res.status(200).json({
status: 200,
data: {},
message: "",
});
} catch (error) {
next(error);
}
}
async resetPassword(req: any, res: ServerResponse, next: NextFunction) {
try {
const {token} = req.params;
const {newPassword} = req.body;
await this.#service.resetPasswordWithToken(token, newPassword);
res.json({
status: 200,
data: {},
message: "رمز عبور ویرایش شد",
});
} catch (err) {
next(err);
}
}
async logout(req: any, res: ServerResponse, next: NextFunction) {
try {
res.clearCookie("accessToken").clearCookie("refreshToken").json({
status: 200,
data: {},
message: "خروج از حساب موفقیت آمیز بود.",
});
} catch (error) {
next(error);
}
}
async refreshToken(req: any, res: ServerResponse, next: NextFunction) {
try {
await this.#service.refreshToken(req, res);
} catch (err) {
next(err);
}
}
async getMe(req: any, res: ServerResponse, next: NextFunction) {
try {
const user = req?.user;
const data = await this.#service.getMe(user);
// if (!data) {
// return res.status(400).json({
// status: 400,
// error: {
// message: "کاربر یافت نشد",
// description: "unauthorized",
// },
// });
// }
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
}
const AuthController = new AuthControllerClass();
export default AuthController;

View File

@@ -0,0 +1,11 @@
export const AUTH_MESSAGES = Object.freeze({
errors:{
401:"حساب کاربری نامعتبر است، لطفا وارد اکانت خود شوید",
400:"دیتای فرم ارسالی نامعتبر است",
404:"نام کاربری یا رمز عبور اشتباه است",
password_failed:"رمز عبور اشتباه است",
},
success:{
200:"با موفقیت وارد شدید"
}
})

View File

@@ -0,0 +1,13 @@
import { authMiddleware, optionalAuthMiddleware } from "@/core/middlewares/auth.middleware";
import AuthController from "../controller/AuthController";
import express from 'express'
const auth_router = express.Router();
auth_router.post("/login", AuthController.login);
auth_router.post("/logout", authMiddleware,AuthController.logout);
auth_router.post("/forgot-password", optionalAuthMiddleware,AuthController.forgotPassword);
auth_router.post("/reset-password/:token", AuthController.resetPassword);
auth_router.get('/get/me',authMiddleware,AuthController.getMe)
auth_router.post('/refresh-token',authMiddleware,AuthController.refreshToken)
export default auth_router;

View File

@@ -0,0 +1,225 @@
import {prisma} from "@/common/lib/prisma";
import {
comparePassword,
generateAccessToken,
generateRefreshToken,
HashPassword,
verifyRefreshToken,
} from "@/common/utils/generate";
import {Controller} from "@/core/controller/main.controller";
import createHttpError from "http-errors";
import {AUTH_MESSAGES} from "../messages";
import crypto from "crypto";
import {handlePrismaError, sendEmail} from "@/common/utils/functions";
import {ServerResponse} from "@/common/types";
import {token_policy} from "@/common/constants/policy";
import {userRequested} from "@/modules/users/types";
class AuthServiceClass extends Controller {
constructor() {
super();
}
async login(username: string, password: string, rememberMe: boolean) {
const user = await prisma.staff.findUnique({
where: {username},
select: {id: true, is_verified: true, password: true, role: true},
});
if (!user?.id) {
throw new createHttpError.NotFound("نام کاربری یا رمز عبور اشتباه است");
}
if (!user.is_verified) {
throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است");
}
const password_match = await comparePassword(password, user.password!);
if (!password_match) {
throw new createHttpError.BadRequest(AUTH_MESSAGES.errors[404]);
}
const accessToken = await generateAccessToken({
id: user.id,
role: user.role,
});
if (rememberMe) {
const refreshToken = await generateRefreshToken(
{id: user.id, role: user.role},
rememberMe
);
try {
await prisma.refreshToken.create({
data: {
expiresAt: new Date(Date.now()),
staffId: user.id,
token: refreshToken,
},
});
} catch (error) {
handlePrismaError(error);
}
return {accessToken, refreshToken};
}
return {accessToken};
}
async forgot_password(username: string, email: string) {
await this.check_user_exist_username(username);
await this.sendResetLink(email);
}
async sendResetLink(email: string) {
const user = await prisma.staff.findFirst({where: {email}});
if (!user) throw new Error("User not found");
if (!user.is_verified) {
throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است");
}
const token = crypto.randomBytes(32).toString("hex");
await prisma.staff.update({
where: {
username: user.username,
id: user.id,
},
data: {
resetPasswordExpires: new Date(Date.now() + 1000 * 60 * 10),
resetPasswordToken: token,
},
});
const resetURL = `${process.env.BASE_DOMAIN}/reset-password/${token}`;
await sendEmail({
to: user.email ?? "",
subject: "Reset Password",
text: `Click to reset your password: ${resetURL}`,
});
return true;
}
async refreshToken(req: any, res: ServerResponse) {
const refreshToken = req.signedCookies?.refreshToken;
if (!refreshToken) {
return res.status(404).json({
status: 404,
error: {
message: "توکنی وجود ندارد",
description: "unauthorized",
},
});
}
const storedToken = await prisma.refreshToken.findUnique({
where: {token: refreshToken},
include: {staff: true},
});
if (!storedToken) {
return res.status(404).json({
status: 404,
error: {
message: "توکن یافت نشد",
description: "unauthorized",
},
});
}
// expire
if (storedToken.expiresAt < new Date()) {
await prisma.refreshToken.delete({
where: {token: refreshToken},
});
return res.status(404).json({
status: 404,
error: {
message: "توکن نامعتبر است",
description: "unauthorized",
},
});
}
const decoded = await verifyRefreshToken(refreshToken);
const accessToken = await generateAccessToken(decoded.user);
res
.cookie("accessToken", accessToken, token_policy.access_token_options)
.status(200)
.json({status: 200, data: {}, message: "Ok"});
}
async resetPasswordWithToken(token: string, newPassword: string) {
const user = await prisma.staff.findFirst({
where: {
resetPasswordToken: token,
resetPasswordExpires: {gt: Date.now().toString()},
},
});
if (!user)
throw new Error(
"مهلت تغییر رمز عبور به پایان رسیده است ، مجددا اقدام کنید"
);
if (!user.is_verified) {
throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است");
}
const newPasswordHash = await HashPassword(newPassword);
await prisma.staff.update({
where: {
username: user.username,
id: user.id,
},
data: {
password: newPasswordHash,
resetPasswordExpires: "",
resetPasswordToken: "",
},
});
return true;
}
async check_user_exist_username(username: string) {
const is_exist = await prisma.staff.findUnique({
where: {username},
});
if (!is_exist) {
return false;
}
return true;
}
async getMe(user: userRequested) {
try {
const data = await prisma.staff.findUnique({
where: {
id: user.id,
},
select: {
id: true,
role: true,
email: true,
username: true,
send_notif_with_email: true,
translations: {
select: {
displayName: true,
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
}
const AuthService = new AuthServiceClass();
export default AuthService;

View File

@@ -0,0 +1,33 @@
import {password_regex} from "@/common/constants/regex";
import Joi from "joi";
export const SigninFormDataValidationSchema = Joi.object({
username: Joi.string().alphanum().required().messages({
"string.base": "نام کاربری نامعتبر است",
"string.empty": "نام کاربری نمیتواند خالی باشد",
"any.required": "نام کاربری الزامیست",
"alphunum": "نام کاربری مطابق استاندارد نیست",
}),
password: Joi.string().required().messages({
"string.base": "رمز عبور الزامیست",
"string.empty": "رمز عبور الزامیست",
"any.required": "رمز عبور الزامیست",
}),
rememberMe: Joi.boolean().optional().messages({
"boolean.base": "مرا بخاطر بسپار نامعتبر است",
}),
});
export const ResetPasswordRequestValidationSchema = Joi.object({
username: Joi.string().alphanum().required().messages({
"string.base": "نام کاربری نامعتبر است",
"string.empty": "نام کاربری نمیتواند خالی باشد",
"any.required": "نام کاربری الزامیست",
alphunum: "نام کاربری مطابق استاندارد نیست",
}),
email: Joi.string().email().required().messages({
"string.base":"فرمت ایمیل نامعتبر است",
"string.email":"ایمیل صحیح نیست",
"any.required":"ایمیل وارد نشده است"
}),
});

View File

@@ -0,0 +1,54 @@
import {Controller} from "@/core/controller/main.controller";
import ConfigsService from "../service/configs.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
class ConfigsControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = ConfigsService;
}
async init(req:any,res:ServerResponse,next:NextFunction){
try {
await this.#service.init();
return res.status(200).json({
status:200,
data:{},
message:"Ok"
})
} catch (error) {
next(error)
}
}
async getAll(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAll();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async update(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.update(req.body);
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
}
const ConfigsController = new ConfigsControllerClass();
export default ConfigsController;

View File

@@ -0,0 +1,9 @@
import express from "express";
import ConfigsController from "../controller/configs.controller";
const configs_router = express.Router();
configs_router.get("/get/all", ConfigsController.getAll);
configs_router.put("/update", ConfigsController.update);
configs_router.get('/init',ConfigsController.init)
export default configs_router;

View File

@@ -0,0 +1,88 @@
import {prisma} from "@/common/lib/prisma";
import {handlePrismaError} from "@/common/utils/functions";
import {Controller} from "@/core/controller/main.controller";
export interface ConfigFormItem {
key: string;
value: string;
description?: string;
}
class ConfigsServiceClass extends Controller {
async init() {
try {
await prisma.panelConfig.createMany({
data: [
{
key: "email.enabled",
value: "true",
type: "BOOLEAN",
description: "Enable/disable email sending module",
},
{
key: "sms.enabled",
value: "false",
type: "BOOLEAN",
description: "Enable/disable SMS sending module",
},
{
key: "upload.document.maxSize",
value: "5242880",
type: "NUMBER",
description: "Max upload Documents size in bytes (5MB)",
},
{
key: "upload.document.formats",
value:
"[application/pdf,application/zip,application/x-rar-compressed,application/dicom,text/plain]",
type: "STRING_ARRAY",
description: "Max upload size in bytes (5MB)",
},
{
key: "upload.profile.maxSize",
value: "5242880",
type: "NUMBER",
description: "Max upload profile picture size in bytes (5MB)",
},
{
key: "upload.profile.formats",
value: "[png,jpg,webp]",
type: "STRING_ARRAY",
description: "Max upload profile picture size in bytes (5MB)",
},
],
});
} catch (error) {
handlePrismaError(error);
}
}
async update(data: ConfigFormItem[]) {
// console.log(data);
try {
await prisma.$transaction(
data.map((item) =>
prisma.panelConfig.update({
where: {key: item.key},
data: {
value: item.value,
description: item.description,
},
})
)
);
} catch (error) {
handlePrismaError(error);
}
}
async getAll() {
try {
const data = await prisma.panelConfig.findMany();
return data;
} catch (error) {
handlePrismaError(error);
}
}
}
const ConfigsService = new ConfigsServiceClass();
export default ConfigsService;

View File

@@ -0,0 +1,62 @@
import {Controller} from "@/core/controller/main.controller";
import CountryService from "../service/country.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import {CreateCountryValidationSchema} from "../validation";
class CountryControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = CountryService;
}
async init(req: any, res: ServerResponse, next: NextFunction) {
try {
await this.#service.init();
return res.status(200).json({
status: 200,
data: {},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async create(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreateCountryValidationSchema.validateAsync(req.body || {}, {
abortEarly: true,
stripUnknown: true,
});
await this.#service.create(req.body);
} catch (error) {
next(error);
}
}
async getAll(req: any, res: ServerResponse, next: NextFunction) {
try {
await this.#service.getAllCountries(req, res);
} catch (error) {
next(error);
}
}
async getAllWithoutPagination(
req: any,
res: ServerResponse,
next: NextFunction
) {
try {
const data = await this.#service.getAllCountriesWithoutPagination();
return res.status(200).json({
status:200,
data,
message:"Ok"
})
} catch (error) {
next(error);
}
}
}
const CountryController = new CountryControllerClass();
export default CountryController;

View File

@@ -0,0 +1,28 @@
import {authMiddleware} from "@/core/middlewares/auth.middleware";
import {role_authorize} from "@/core/middlewares/role-authorize.middleware";
import express from "express";
import CountryController from "../countroller/country.controller";
const country_router = express.Router();
country_router.post(
"/create",
authMiddleware,
role_authorize("developer", "admin"),
CountryController.create
);
country_router.get(
"/get/items",
authMiddleware,
role_authorize("developer", "admin"),
CountryController.getAllWithoutPagination
);
country_router.get(
"/get/all",
authMiddleware,
role_authorize("developer", "admin"),
CountryController.getAll
);
country_router.get("/init", CountryController.init);
export default country_router;

View File

@@ -0,0 +1,49 @@
import {Controller} from "@/core/controller/main.controller";
import {CreateCountryDataTypes} from "../types";
import {buildPagination, handlePrismaError} from "@/common/utils/functions";
import {prisma} from "@/common/lib/prisma";
import {ServerResponse} from "@/common/types";
import { allCountries } from "@/common/constants/variables";
class CountryServiceClass extends Controller {
async init() {
try {
await prisma.countries.createMany({
data: allCountries.map((country) => ({
name: country.label,
callCode: country.code,
})),
});
} catch (error) {
handlePrismaError(error);
}
}
async create(data: CreateCountryDataTypes) {
const {name, callCode} = data;
try {
await prisma.countries.create({
data: {
name,
callCode,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async getAllCountriesWithoutPagination() {
try {
const data = await prisma.countries.findMany();
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getAllCountries(req: any, res: ServerResponse) {
await buildPagination(req, res, "countries");
}
}
const CountryService = new CountryServiceClass();
export default CountryService;

View File

@@ -0,0 +1,4 @@
export interface CreateCountryDataTypes{
name:string,
callCode:string
}

View File

@@ -0,0 +1,16 @@
import Joi from "joi";
export const CreateCountryValidationSchema = Joi.object({
name: Joi.string().required().messages({
"string.base": "فرمت نام کشور اشتباه است",
"string.empty": "نام کشور نمیتواند خالی باشد",
"any.required": "نام کشور وارد نشده است",
}),
callCode: Joi.string()
.required()
.messages({
"string.base": "فرمت کد کشور اشتباه است",
"string.empty": "کد کشور نمیتواند خالی باشد",
"any.required": "کد کشور وارد نشده است",
}),
});

View File

@@ -0,0 +1,61 @@
import {Controller} from "@/core/controller/main.controller";
import DefaultInfoService from "../service/default-info.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import UpdateDefaultInfoValidationSchema from "../validation";
class DefaultInfoControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = DefaultInfoService;
}
async getAll(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAll();
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async update(req: any, res: ServerResponse, next: NextFunction) {
try {
await UpdateDefaultInfoValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.update("SITE_DEFAULTS", req.body);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ثبت شد",
});
} catch (error) {
next(error);
}
}
async initDefaultInfo(req:any,res:ServerResponse,next:NextFunction){
try {
await this.#service.initDefaultInfo();
return res.status(200).json({
status:200,
data:{},
message:"ویرایش شد"
})
} catch (error) {
next(error)
}
}
}
const DefaultInfoController = new DefaultInfoControllerClass();
export default DefaultInfoController;

View File

@@ -0,0 +1,24 @@
import {authMiddleware} from "@/core/middlewares/auth.middleware";
import {role_authorize} from "@/core/middlewares/role-authorize.middleware";
import express from "express";
import DefaultInfoController from "../controller/default-info.controller";
const default_info_router = express.Router();
default_info_router.get(
"/get/all",
authMiddleware,
role_authorize("admin", "developer"),
DefaultInfoController.getAll
);
default_info_router.put(
"/update",
authMiddleware,
role_authorize("admin", "developer"),
DefaultInfoController.update
);
default_info_router.get('/init',DefaultInfoController.initDefaultInfo)
export default default_info_router;

View File

@@ -0,0 +1,125 @@
import { prisma } from "@/common/lib/prisma";
import { handlePrismaError } from "@/common/utils/functions";
import { Controller } from "@/core/controller/main.controller";
export interface DefaultTranslationInput {
languageId: number;
address: string;
underLogoText: string;
aboutUsText: string;
patientsRights: string;
}
export interface DefaultFormValues {
email: string;
instagramLink: string;
linkedinLink: string;
hospitalPhone: string;
mapAddress: string;
logo?: string | null;
translations: DefaultTranslationInput[]; // به جای فیلد تکی
}
class DefaultInfoServiceClass extends Controller {
async getAll() {
try {
const data = await prisma.default.findUnique({
where: { id: "SITE_DEFAULTS" },
include: {
translations: {
select: {
address: true,
aboutUsText: true,
underLogoText: true,
languageId: true,
language: {
select: {
title: true,
slug: true,
},
},
},
},
},
});
// console.log(data)
return data;
} catch (error) {
handlePrismaError(error);
}
}
async update(id: "SITE_DEFAULTS", data: DefaultFormValues) {
try {
await prisma.default.upsert({
where: { id },
update: {
email: data.email,
hospitalPhone: data.hospitalPhone,
instagramLink: data.instagramLink,
linkedinLink: data.linkedinLink,
mapAddress: data.mapAddress,
logoUrl: data.logo,
},
create: {
id,
email: data.email,
hospitalPhone: data.hospitalPhone,
instagramLink: data.instagramLink,
linkedinLink: data.linkedinLink,
mapAddress: data.mapAddress,
logoUrl: data.logo,
},
});
// بروزرسانی یا ایجاد ترجمه‌ها
for (const translation of data.translations) {
await prisma.defaultTranslation.upsert({
where: {
defaultId_languageId: {
defaultId: id,
languageId: translation.languageId,
},
},
update: {
address: translation.address,
underLogoText: translation.underLogoText,
aboutUsText: translation.aboutUsText,
patientsRights: translation.patientsRights,
},
create: {
defaultId: id,
languageId: translation.languageId,
address: translation.address,
underLogoText: translation.underLogoText,
aboutUsText: translation.aboutUsText,
patientsRights: translation.patientsRights,
},
});
}
return true;
} catch (error) {
handlePrismaError(error);
}
}
async initDefaultInfo() {
try {
await prisma.default.create({
data: {
email: "ipd@shomal.hospital",
hospitalPhone: "011-4492",
instagramLink: "https://instagram.com/shomalhospital",
mapAddress: "asd",
linkedinLink:
"https://www.linkedin.com/in/shomal-amol-hospital-7a1699392/",
logoUrl: "",
},
});
} catch (error) {
handlePrismaError(error);
}
}
}
const DefaultInfoService = new DefaultInfoServiceClass();
export default DefaultInfoService;

View File

@@ -0,0 +1,62 @@
const Joi = require("joi");
export const translationSchema = Joi.object({
address: Joi.string().required().messages({
"string.base": "آدرس باید رشته باشد",
"any.required": "آدرس الزامیست",
}),
underLogoText: Joi.string().required().messages({
"string.base": "متن زیر لوگو باید رشته باشد",
"any.required": "متن زیر لوگو الزامیست",
}),
});
/* ---------- Main Schema ---------- */
export const UpdateDefaultInfoValidationSchema = Joi.object({
email: Joi.string()
.email({tlds: {allow: false}})
.required()
.messages({
"string.email": "فرمت ایمیل صحیح نیست",
"any.required": "ایمیل الزامی است",
}),
hospitalPhone: Joi.string()
.pattern(/^[0-9+\-() ]+$/)
.min(8)
.required()
.messages({
"string.pattern.base": "شماره تلفن فقط می‌تواند شامل عدد و + - ( ) باشد",
"string.min": "شماره تلفن کوتاه است",
"any.required": "شماره تلفن الزامی است",
}),
mapAddress: Joi.string().min(5).required().messages({
"string.min": "آدرس خیلی کوتاه است",
"any.required": "آدرس الزامی است",
}),
instagramLink: Joi.string().uri().allow(null, "").messages({
"string.uri": "لینک اینستاگرام معتبر نیست",
}),
linkedinLink: Joi.string().uri().allow(null, "").messages({
"string.uri": "لینک لینکدین معتبر نیست",
}),
logoUrl: Joi.string().uri().allow(null, "").messages({
"string.uri": "لینک لوگو معتبر نیست",
}),
translations: Joi.array()
.items(translationSchema)
.min(1)
.required()
.messages({
"array.min": "حداقل یک ترجمه الزامی است",
"any.required": "وارد کردن ترجمه ها الزامیست",
}),
});
export default UpdateDefaultInfoValidationSchema;

View File

@@ -0,0 +1,100 @@
import {Controller} from "@/core/controller/main.controller";
import ExpertiseService from "../service/expertise.service";
import {createExpertiseValidationSchema} from "../validation";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
class ExpertiseControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = ExpertiseService;
}
async getExpertiseWithoutPagination(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getExpertiseWithoutPagination();
return res.status(200).json({
status: 200,
data,
message: "ok",
});
} catch (error) {
next(error);
}
}
async getExpertise(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getExpertises(req);
return res.status(200).json({
status: 200,
data,
message: "ok",
});
} catch (error) {
next(error);
}
}
async getExpertiseById(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getExpertiseById(req);
return res.status(200).json({
status: 200,
data: {...data},
message: "ok",
});
} catch (error) {
next(error);
}
}
async createExpertise(req: any, res: ServerResponse, next: NextFunction) {
try {
await createExpertiseValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.createExpertise(req.body || {});
return res.status(200).json({
status: 200,
data: {},
message: "انجام شد",
});
} catch (error) {
next(error);
}
}
async updateExpertise(req: any, res: ServerResponse, next: NextFunction) {
try {
await createExpertiseValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.updateExpertise(req);
return res.status(200).json({
status: 200,
data: {},
message: "ویرایش شد",
});
} catch (error) {
next(error);
}
}
async deleteExpertise(req: any, res: ServerResponse, next: NextFunction) {
try {
const id = req?.params?.id;
await this.#service.deleteExpertise(id);
return res.status(200).json({
status: 200,
data: {},
message: "حذف شد",
});
} catch (error) {
next(error);
}
}
}
const ExpertiseController = new ExpertiseControllerClass();
export default ExpertiseController;

View File

@@ -0,0 +1,13 @@
import express from "express";
import ExpertiseController from "../controller/expertise.controller";
import { checkLanguageSlug } from "@/core/middlewares/check-language.middleware";
const expertise_router = express.Router();
expertise_router.get("/:lang/get/all",checkLanguageSlug, ExpertiseController.getExpertise);
expertise_router.get("/:lang/get/all/list",checkLanguageSlug, ExpertiseController.getExpertiseWithoutPagination);
expertise_router.get("/get/single/:id", ExpertiseController.getExpertiseById);
expertise_router.post("/create", ExpertiseController.createExpertise);
expertise_router.put("/update/:id", ExpertiseController.updateExpertise);
expertise_router.delete("/delete/:id", ExpertiseController.deleteExpertise);
export default expertise_router;

View File

@@ -0,0 +1,217 @@
import {Controller} from "@/core/controller/main.controller";
import {prisma} from "@/common/lib/prisma";
import {handlePrismaError} from "@/common/utils/functions";
import createHttpError from "http-errors";
interface ExpertiseTranslation {
displayName: string;
lang_id: string;
}
interface createExpertiseDataBody {
slug: string;
translations: ExpertiseTranslation[];
}
class ExpertiseServiceClass extends Controller {
async createExpertise(data: createExpertiseDataBody) {
const {translations, slug} = data;
try {
await prisma.expertise.create({
data: {
slug,
translations: {
create: translations,
},
},
include: {translations: true},
});
return true;
} catch (err) {
handlePrismaError(err);
}
}
async getExpertiseWithoutPagination() {
// const where: any = {
// ...(id && {pid: id}),
// };
try {
const data = await prisma.expertise.findMany({
include: {
translations: {
select: {
displayName: true,
lang_id: true,
lang: true,
expertise: true,
id: true,
},
},
},
});
return data;
} catch (error) {
console.log(error);
handlePrismaError(error);
}
}
async getExpertises(req: any) {
const langId = req?.langId;
const page = req?.query?.page;
const limit = req?.query?.limit;
const slug = req?.query?.slug;
const search = req?.query?.search;
const skip = (page - 1) * limit ;
const id = req?.query?.id;
// const where: any = {
// ...(id && {pid: id}),
// };
try {
const [data, total] = await Promise.all([
prisma.expertise.findMany({
skip,
take: +limit,
orderBy: {id: "desc"},
where: {
...(id && {id}),
...(slug && {slug}),
translations: {
some: {
lang_id: langId,
...(search && {
displayName: {
contains: search,
mode: "insensitive",
},
}),
},
},
},
select: {
id: true,
slug: true,
translations: {
where: {
lang_id: langId,
},
select: {
displayName: true,
lang: {
select: {
title: true,
},
},
},
},
},
}),
await prisma.expertise.count({
where: {
...(id && {id}),
...(slug && {slug}),
translations: {
some: {
...(langId && {lang_id: langId}),
...(search && {
displayName: {
contains: search,
mode: "insensitive",
},
}),
},
},
},
}),
]);
return {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
data,
};
} catch (error) {
console.log(error);
handlePrismaError(error);
}
}
async getExpertiseById(req: any) {
const id = req.params?.id;
const lang = req.query?.lang;
try {
const expertise = await prisma.expertise.findUnique({
where: {id: Number(id)},
include: {
translations: lang ? {where: {lang}} : true,
},
});
if (!expertise) {
throw new createHttpError.NotFound("پیدا نشد");
}
return expertise;
} catch (err) {
handlePrismaError(err);
}
}
async updateExpertise(req: any) {
const id = req.params?.id;
const slug = req.body?.slug;
const translations = req.body?.translations;
try {
const expertise = await prisma.expertise.update({
where: {id: Number(id)},
data: {
slug,
translations: {
upsert: translations.map((t: ExpertiseTranslation) => ({
where: {
expertiseId_lang: {
expertiseId: Number(id),
lang: t.lang_id,
},
},
update: {
displayName: t.displayName,
},
create: {
lang: t.lang_id,
displayName: t.displayName,
},
})),
},
},
include: {translations: true},
});
return true;
} catch (err) {
console.log(err);
handlePrismaError(err);
}
}
async deleteExpertise(id: number) {
try {
await prisma.expertise.delete({
where: {id: Number(id)},
});
return true;
} catch (error) {
handlePrismaError(error);
}
}
}
const ExpertiseService = new ExpertiseServiceClass();
export default ExpertiseService;

View File

@@ -0,0 +1,30 @@
import Joi from "joi";
const translationSchema = Joi.object({
displayName: Joi.string().required().messages({
"string.base": "display name must be a string",
"string.length": "display name code must be 2 characters",
"any.required": "display name is required",
}),
lang_id: Joi.number().required().messages({
"any.required": "Language is required",
}),
});
export const createExpertiseValidationSchema = Joi.object({
slug: Joi.string().required().messages({
"string.base": "فرمت اسلاگ اشتباه است",
"string.empty": "اسلاگ نمیتواند خالی باشد",
"any.required": "اسلاگ وارد نشده است",
}),
translations: Joi.array()
.items(translationSchema)
.min(1)
.required()
.messages({
"array.base": "Translations must be an array",
"array.min": "At least one translation is required",
"any.required": "Translations are required",
}),
});

View File

@@ -0,0 +1,80 @@
import {Controller} from "@/core/controller/main.controller";
import LanguageService from "../service/language.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import {CreateLanguageValidationSchema} from "../validation";
class LanguageControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = LanguageService;
}
async getAllLanguage(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAllLanguage(req);
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async createLanguage(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreateLanguageValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.createLanguage(req.body || {});
return res.status(200).json({
status: 200,
data: {},
message: "ایجاد شد",
});
} catch (error) {
next(error);
}
}
async updateLanguage(req: any, res: ServerResponse, next: NextFunction) {
try {
const id = req?.params?.id;
await CreateLanguageValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.updateLanguage(req.body, +id);
return res.status(200).json({
status: 200,
data: {},
message: "ویرایش شد",
});
} catch (error) {
next(error);
}
}
async deleteLanguage(req: any, res: ServerResponse, next: NextFunction) {
try {
const id = req?.params?.id;
await this.#service.delete(id);
return res.status(200).json({
status: 200,
data: {},
message: "حذف شد",
});
} catch (error) {
next(error);
}
}
}
const LanguageController = new LanguageControllerClass();
export default LanguageController;

View File

@@ -0,0 +1,29 @@
import { role_authorize } from "@/core/middlewares/role-authorize.middleware";
import { authMiddleware } from "./../../../core/middlewares/auth.middleware";
import express from "express";
import LanguageController from "../controller/language.controller";
const language_router = express.Router();
language_router.post(
"/create",
authMiddleware,
role_authorize("developer"),
LanguageController.createLanguage,
);
language_router.put(
"/update/:id",
authMiddleware,
role_authorize("developer"),
LanguageController.updateLanguage,
);
language_router.delete(
"/delete/:id",
authMiddleware,
role_authorize("developer"),
LanguageController.deleteLanguage,
);
language_router.get("/get/all", LanguageController.getAllLanguage);
export default language_router;

View File

@@ -0,0 +1,90 @@
import {prisma} from "@/common/lib/prisma";
import {handlePrismaError} from "@/common/utils/functions";
import {createSlug} from "@/common/utils/generate";
import {Controller} from "@/core/controller/main.controller";
import { Language } from "@/generated/prisma/client";
import createHttpError from "http-errors";
class LanguageServiceClass extends Controller {
async getAllLanguage(req: any) {
const search = req?.query?.search;
try {
const data = await prisma.language.findMany({
where: {
title: {contains: search, mode: "insensitive"},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async createLanguage(data: Language) {
let existing_slug;
try {
existing_slug = await prisma.language.findUnique({
where: {
title: data.title,
slug: data.slug,
},
});
} catch (error) {
handlePrismaError(error);
}
if (existing_slug) {
throw new createHttpError.Conflict("دیتای تکراری");
}
try {
await prisma.language.create({
data: {
title: data.title,
slug: data.slug,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async updateLanguage(data: Language, id: number) {
const language = await this.isExistLanguage(id);
if (!language) {
throw new createHttpError.NotFound("شناسه یافت نشد");
}
const slug = await createSlug(data.title);
if (language.slug === slug) {
throw new createHttpError.Conflict("دیتای تکراری");
}
try {
await prisma.language.update({
where: {
id,
},
data: {
...(data.title && {title: data.title}),
...(slug && {slug}),
},
});
} catch (error) {
handlePrismaError(error);
}
}
async delete(id: string) {
try {
await prisma.language.delete({where: {id: +id}});
return true;
} catch (error) {
handlePrismaError(error);
}
}
}
const LanguageService = new LanguageServiceClass();
export default LanguageService;

View File

@@ -0,0 +1,14 @@
import Joi from "joi";
export const CreateLanguageValidationSchema = Joi.object({
title: Joi.string().required().messages({
"string.base": "فرمت عنوان اشتباه است",
"string.empty": "عنوان نمیتواند خالی باشد",
"any.required": "عنوان وارد نشده است",
}),
slug: Joi.string().required().messages({
"string.base": "فرمت اسلاگ اشتباه است",
"string.empty": "اسلاگ نمیتواند خالی باشد",
"any.required": "اسلاگ وارد نشده است",
}),
});

View File

@@ -0,0 +1,15 @@
import { Controller } from "@/core/controller/main.controller";
import LogsService from "../service/logs.service";
class LogsControllerClass extends Controller{
#service;
constructor(){
super();
this.#service = LogsService
}
}
const LogsController = new LogsControllerClass();
export default LogsController;

View File

@@ -0,0 +1,10 @@
import { Controller } from "@/core/controller/main.controller";
class LogsServiceClass extends Controller {
}
const LogsService = new LogsServiceClass();
export default LogsService

View File

@@ -0,0 +1,89 @@
import {Controller} from "@/core/controller/main.controller";
import MedicalPackageService from "../service/medical-package.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import CreateMedicalPackageValidationSchema from "../validation";
class MedicalPackageControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = MedicalPackageService;
}
async create(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreateMedicalPackageValidationSchema.validateAsync(req.body || {});
await this.#service.create(req.body);
return res.status(200).json({
status: 200,
data: {},
message: "انجام شد",
});
} catch (error) {
next(error);
}
}
async getAllWithPagination(
req: any,
res: ServerResponse,
next: NextFunction
) {
try {
const data = await this.#service.getAllWithPagination(req, res);
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getAllParentsWithoutPagination(
req: any,
res: ServerResponse,
next: NextFunction
) {
try {
const data = await this.#service.getAllParentsWithoutPagination();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getSingleMedicalPackage(req:any,res:ServerResponse,next:NextFunction){
try {
const id = req?.params?.id;
const data = await this.#service.getSingleMedicalPackage(req,id)
return res.status(200).json({
status:200,
data,
message:"Ok"
})
} catch (error) {
next(error)
}
}
async delete(req:any,res:ServerResponse,next:NextFunction){
try {
const id = req?.params?.id;
await this.#service.delete(id)
return res.status(200).json({
status:200,
data:{},
message:"حذف شد"
})
} catch (error) {
next(error)
}
}
}
const MedicalPackageController = new MedicalPackageControllerClass();
export default MedicalPackageController;

View File

@@ -0,0 +1,20 @@
import express from "express";
import MedicalPackageController from "../controller/medical-package.controller";
import {checkLanguageSlug} from "@/core/middlewares/check-language.middleware";
const medical_package_router = express.Router();
medical_package_router.post("/create", MedicalPackageController.create);
medical_package_router.get(
"/:lang/get/all",
checkLanguageSlug,
MedicalPackageController.getAllWithPagination
);
medical_package_router.get(
"/:lang/get/all/parent",
checkLanguageSlug,
MedicalPackageController.getAllParentsWithoutPagination
);
medical_package_router.get('/:lang/get/single/:id',checkLanguageSlug,MedicalPackageController.getSingleMedicalPackage)
medical_package_router.delete('/delete/:id',MedicalPackageController.delete)
export default medical_package_router;

View File

@@ -0,0 +1,182 @@
import {prisma} from "@/common/lib/prisma";
import {ServerResponse} from "@/common/types";
import {handlePrismaError} from "@/common/utils/functions";
import {Controller} from "@/core/controller/main.controller";
class MedicalPackageServiceClass extends Controller {
async create(data: any) {
const {translations, id, icon, parent_id, priority, thumbnail_id,price} = data;
try {
await prisma.medicalPackage.create({
data: {
icon,
translations: {
create: translations,
},
priority,
price,
parent_id: parent_id ? +parent_id : null,
...(thumbnail_id && {thumbnail_id: +thumbnail_id}),
},
include: {
translations: true,
},
});
return true;
} catch (error) {
console.log(error);
handlePrismaError(error);
}
}
async getAllWithPagination(req: any, res: ServerResponse) {
const langId = req?.langId;
const page = req?.query?.page;
const limit = req?.query?.limit;
const skip = (page - 1) * limit;
const search = req?.query?.search;
try {
const [data, total] = await Promise.all([
prisma.medicalPackage.findMany({
skip,
take: +limit,
where: {
translations: {
some: {
lang_id: langId,
...(search && {
displayName: {
contains: search,
mode: "insensitive",
},
}),
},
},
},
select: {
id: true,
icon: true,
priority: true,
thumbnail_id: true,
parent_id: true,
price:true,
thumbnail: {
select: {
filename: true,
fileUrl:true,
fileKey:true,
},
},
children: true,
translations: {
where: {
lang_id: langId,
},
select: {
lang_id: true,
content: true,
title: true,
},
},
},
}),
await prisma.medicalPackage.count({
where: {
translations: {
some: {
lang_id: langId,
...(search && {
displayName: {
contains: search,
mode: "insensitive",
},
}),
},
},
},
}),
]);
return {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
data,
};
} catch (error) {
handlePrismaError(error);
}
}
async getAllParentsWithoutPagination() {
try {
const data = await prisma.medicalPackage.findMany({
where: {
parent_id: null,
},
include: {
translations: true,
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getSingleMedicalPackage(req: any, id: string) {
const langId = req?.langId;
try {
const data = await prisma.medicalPackage.findUnique({
where: {
id: +id,
translations: {
some: {
lang_id: +langId,
},
},
},
include: {
translations: {
select: {
lang_id: true,
title: true,
content: true,
},
},
thumbnail:{
select:{
fileKey:true,
filename:true,
fileUrl:true
}
}
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async delete(id: string) {
try {
await prisma.medicalPackage.delete({
where: {
id: +id,
},
});
return true;
} catch (error) {
handlePrismaError(error);
}
}
}
const MedicalPackageService = new MedicalPackageServiceClass();
export default MedicalPackageService;

View File

@@ -0,0 +1,49 @@
import Joi from "joi";
const translationSchema = Joi.object({
title: Joi.string().required().messages({
"string.base": "عنوان می بایست رشته باشد",
"string.empty": "عنوان نمیتواند خالی باشد",
"any.required": "عنوان الزامیست",
}),
content: Joi.string().required().messages({
"string.base": "محتوا می بایست رشته باشد",
"string.empty": "محتوا نمیتواند خالی باشد",
"any.required": "محتوا الزامیست",
}),
lang_id: Joi.number().required().messages({
"any.required": "Language is required",
}),
});
const CreateMedicalPackageValidationSchema = Joi.object({
icon: Joi.string().optional().allow("").messages({
"string.base": "آیکون می بایست رشته باشد",
"string.empty": "آیکون نمیتواند خالی باشد",
}),
priority: Joi.number().required().messages({
"number.base": "اولویت نمایش می بایست عدد باشد",
"any.required": "اولویت نمایش الزامیست",
}),
parent_id: Joi.number().optional().allow(null).messages({
"number.base": "شناسه والد می بایست عدد باشد",
}),
price: Joi.string().required().allow("").messages({
"any.required": "قيمت الزاميست",
}),
thumbnail_id: Joi.number().optional().allow(null).messages({
"number.base": "شناسه عکس شاخص می بایست عدد باشد",
"number.empty": "شناسه عکس شاخص نمیتواند خالی باشد",
"any.required": "عکس شاخص انتخاب نشده است",
}),
translations: Joi.array()
.items(translationSchema)
.min(1)
.required()
.messages({
"array.base": "Translations must be an array",
"array.min": "At least one translation is required",
"any.required": "Translations are required",
}),
});
export default CreateMedicalPackageValidationSchema;

View File

@@ -0,0 +1,101 @@
import {Controller} from "@/core/controller/main.controller";
import OnlineCaseService from "../service/online-case.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import {
createOnlineCaseValidationSchema,
createOnlineCaseWithFormValidationSchema,
} from "../validation";
class OnlineCaseControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = OnlineCaseService;
}
async getAllOnlineCases(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAllOnlineCases(req,res);
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getSingleOnlineCase(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getSingleOnlineCase(req);
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async createOnlineCaseWithForm(
req: any,
res: ServerResponse,
next: NextFunction
) {
try {
const userId = req.params?.id
await createOnlineCaseWithFormValidationSchema.validateAsync(
req.body || {},
{
abortEarly: true,
stripUnknown: true,
}
);
await this.#service.createOnlineCaseWithForm(req.body || {},userId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ایجاد شد",
});
} catch (error) {
next(error);
}
}
async createOnlineCase(req: any, res: ServerResponse, next: NextFunction) {
try {
const patientId = req?.params?.id;
await createOnlineCaseValidationSchema.validateAsync(req.body || {}, {
abortEarly: true,
stripUnknown: true,
});
await this.#service.createOnlineCase(req.body, patientId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ایجاد شد",
});
} catch (error) {
next(error);
}
}
async updateOnlineCase(req: any, res: ServerResponse, next: NextFunction) {
try {
const caseId = req?.params?.id;
await createOnlineCaseValidationSchema.validateAsync(req.body || {}, {
abortEarly: true,
stripUnknown: true,
});
await this.#service.updateOnlineCase(req.body, caseId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ویرایش شد",
});
} catch (error) {
next(error);
}
}
}
const OnlineCaseController = new OnlineCaseControllerClass();
export default OnlineCaseController;

View File

@@ -0,0 +1,18 @@
import express from "express";
import OnlineCaseController from "../controller/online-case.controller";
const onlineCase_router = express.Router();
onlineCase_router.get("/get/all", OnlineCaseController.getAllOnlineCases);
onlineCase_router.post("/create", OnlineCaseController.createOnlineCase);
onlineCase_router.post(
"/create/form",
OnlineCaseController.createOnlineCaseWithForm
);
onlineCase_router.get(
"/get/single/:id",
OnlineCaseController.getSingleOnlineCase
);
onlineCase_router.put("/update/:id", OnlineCaseController.updateOnlineCase);
export default onlineCase_router;

View File

@@ -0,0 +1,162 @@
import {Controller} from "@/core/controller/main.controller";
import {OnlineCaseDataBody, UpdateOnlineCaseDataBody} from "../types";
import {prisma} from "@/common/lib/prisma";
import {
buildPagination,
generatePatientPID,
handlePrismaError,
} from "@/common/utils/functions";
import {generateIPDReceptionCode} from "@/common/utils/generate";
import {createPatientDataBody} from "@/modules/patient/types";
import {ServerResponse} from "@/common/types";
class OnlineCaseServiceClass extends Controller {
constructor() {
super();
}
async createOnlineCase(data: OnlineCaseDataBody, id: string) {
await this.isPatientExist(id);
const trackingCode = await generateIPDReceptionCode();
let newOnlineCase;
try {
newOnlineCase = await prisma.onlineCase.create({
data: {
trackingCode,
message: data.message,
patientId: id,
specialty: data.specialty,
formData: JSON.stringify(data.formData),
status: "NEW",
},
});
} catch (error) {
handlePrismaError(error);
}
await this.updateCaseStatus(newOnlineCase?.id!, "NEW");
}
async updateOnlineCase(data: UpdateOnlineCaseDataBody, caseId: string) {
const patient = await this.isPatientExist(data.patientId);
const onlineCase = await this.isOnlineCaseExist(caseId);
try {
await prisma.onlineCase.update({
where: {
id: patient?.id,
},
data: {
...(data?.message && {message: data.message}),
...(data.patientId && {patientId: data.patientId}),
...(data?.specialty && {specialty: data.specialty}),
...(data?.formData && {formData: JSON.stringify(data.formData)}),
},
});
} catch (error) {
handlePrismaError(error);
}
await this.updateCaseStatus(onlineCase.id, data.status);
}
async contactedOnlineCase(caseId: string) {
await this.updateCaseStatus(caseId, "CONTACTED");
}
async createOnlineCaseWithForm(
data: createPatientDataBody & OnlineCaseDataBody,
userId: string
) {
let patient;
let newOnlineCase;
try {
patient = await prisma.patient.create({
data: {
firstName: data.firstName,
lastName: data.lastName,
age: data?.age,
nationalityCode: data.nationalityCode,
address: data.address,
birthDate: data.birthDate,
nationalityId: +data.nationality!,
pid: await generatePatientPID(),
phone: data.phone,
email: data.email,
preferredLanguage: data.preferredLanguage,
},
select: {
id: true,
},
});
} catch (error) {
handlePrismaError(error);
}
const trackingCode = await generateIPDReceptionCode();
try {
newOnlineCase = await prisma.onlineCase.create({
data: {
trackingCode,
message: data.message,
patientId: patient?.id!,
specialty: data.specialty,
formData: JSON.stringify(data.formData),
status: "NEW",
},
});
} catch (error) {
handlePrismaError(error);
}
// create new CaseStatusHistory
if (newOnlineCase?.id) {
await this.updateCaseStatus(newOnlineCase.id, "NEW");
}
}
async getAllOnlineCases(req: any, res: ServerResponse) {
return buildPagination(req, res, "onlineCase", {
include: {
reviews: {
select: {
translations: {
select: {
lang: true,
note: true,
result: true,
id: true,
},
},
},
},
},
});
}
async getSingleOnlineCase(id: string) {
try {
const data = await prisma.onlineCase.findUnique({
where: {id},
include: {
reviews: {
select: {
caseId: true,
translations: {
select: {
lang: true,
note: true,
result: true,
},
},
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
}
const OnlineCaseService = new OnlineCaseServiceClass();
export default OnlineCaseService;

View File

@@ -0,0 +1,14 @@
import {CaseStatus} from "@/generated/prisma/enums";
export interface OnlineCaseDataBody {
message?: string;
specialty?: string;
formData?: string;
}
export interface UpdateOnlineCaseDataBody {
message?: string;
specialty?: string;
formData?: string;
status: CaseStatus;
patientId: string;
}

View File

@@ -0,0 +1,83 @@
import {CaseStatusConst} from "@/common/types";
import Joi from "joi";
export const createOnlineCaseWithFormValidationSchema = Joi.object({
fullName: Joi.string().required().messages({
"any.required": "نام بیمار الزامیست",
"string.base": "نام بیمار نامعتبر است",
"string.empty": "نام بیمار نمی تواند خالی باشد",
}),
lastName: Joi.string().required().messages({
"any.required": "نام بیمار الزامیست",
"string.base": "نام بیمار نامعتبر است",
"string.empty": "نام بیمار نمی تواند خالی باشد",
}),
nationality: Joi.string().optional(),
countryCode: Joi.string().optional(),
phone: Joi.string().optional(),
email: Joi.string().optional(),
preferredLanguage: Joi.string().optional().messages({
"string.base": "زبان ترجیحی معتبر نیست",
}),
age: Joi.number().integer().min(0).max(150).optional().messages({
"number.base": "سن باید عدد باشد",
"number.integer": "سن باید عدد صحیح باشد",
"number.min": "سن نمی‌تواند کمتر از ۰ باشد",
"number.max": "سن نمی‌تواند بیشتر از 150 باشد",
}),
message: Joi.string().optional().allow("").messages({
"string.base": "پیغام باید رشته باشد",
}),
specialty: Joi.string().optional().allow("").messages({
"string.base": "تخصص باید رشته باشد",
}),
formData: Joi.object().optional().messages({
"object.base": "فرم داده‌ها باید یک شیء JSON باشد",
}),
status: Joi.string()
// .valid(...Object(CaseStatusConst).values())
.optional()
.messages({
"any.only": `وضعیت باید یکی از مقادیر معتبر باشد: ${Object.values(
CaseStatusConst
).join(", ")}`,
"string.base": "وضعیت باید رشته باشد",
}),
})
.or("phone", "email")
.messages({
"object.missing": "حداقل یکی از فیلدهای شماره تماس یا ایمیل باید وارد شود.",
});
export const createOnlineCaseValidationSchema = Joi.object({
message: Joi.string().optional().allow("").messages({
"string.base": "پیغام باید رشته باشد",
}),
specialty: Joi.string().optional().allow("").messages({
"string.base": "تخصص باید رشته باشد",
}),
formData: Joi.object().optional().messages({
"object.base": "فرم داده‌ها باید یک شیء JSON باشد",
}),
status: Joi.string()
// .valid(...Object(CaseStatusConst).values())
.optional()
.messages({
"any.only": `وضعیت باید یکی از مقادیر معتبر باشد: ${Object.values(
CaseStatusConst
).join(", ")}`,
"string.base": "وضعیت باید رشته باشد",
}),
})
.or("phone", "email")
.messages({
"object.missing": "حداقل یکی از فیلدهای شماره تماس یا ایمیل باید وارد شود.",
});

View File

@@ -0,0 +1,129 @@
import {Controller} from "@/core/controller/main.controller";
import PatientService from "../services/patient.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import {CreatePatientValidationSchema} from "../validation";
class PatientControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = PatientService;
}
async getAllPatientsWithoutPagination(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAllPatientWithoutPagination(req);
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getAllPatients(req: any, res: ServerResponse, next: NextFunction) {
try {
const user =req?.user
const data = await this.#service.getAllPatients(req,res,user);
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getSinglePatient(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id;
const data = await this.#service.getSinglePatient(targetId);
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async createPatient(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreatePatientValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.createPatient(req.body);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ایجاد شد",
});
} catch (error) {
next(error);
}
}
async updatePatient(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id;
await CreatePatientValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
await this.#service.updatePatient(req.body, targetId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ویرایش شد",
});
} catch (error) {
next(error);
}
}
async deletePatient(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id;
console.log(targetId)
await this.#service.deletePatient(targetId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت حذف گردید",
});
} catch (error) {
next(error);
}
}
async restorePatient(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id;
await this.#service.restorePatient(targetId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت بازگردانی شد",
});
} catch (error) {
next(error);
}
}
// async exportPatientsExcel(req: any, res: ServerResponse, next: NextFunction) {
// try {
// await exportExcel(res, "patient");
// } catch (error) {
// next(error);
// }
// }
}
const PatientController = new PatientControllerClass();
export default PatientController;

View File

@@ -0,0 +1,39 @@
import {role_authorize} from "@/core/middlewares/role-authorize.middleware";
import express from "express";
import PatientController from "../controllers/patient.controller";
import {authMiddleware} from "@/core/middlewares/auth.middleware";
const patients_router = express.Router();
patients_router.get(
"/get/all",
authMiddleware,
role_authorize("developer", "admin", "coordinator", "doctor"),
PatientController.getAllPatients
);
patients_router.get("/get/single/:id", PatientController.getSinglePatient);
patients_router.put(
"/update/:id",
authMiddleware,
role_authorize("admin", "developer", "coordinator"),
PatientController.updatePatient
);
patients_router.put(
"/restore/:id",
authMiddleware,
role_authorize("admin", "developer", "coordinator"),
PatientController.restorePatient
);
patients_router.delete(
"/delete/:id",
authMiddleware,
role_authorize("admin", "developer"),
PatientController.deletePatient
);
patients_router.post(
"/create",
authMiddleware,
role_authorize("admin", "developer", "coordinator"),
PatientController.createPatient
);
export default patients_router;

View File

@@ -0,0 +1,213 @@
import {Controller} from "@/core/controller/main.controller";
import {createPatientDataBody} from "../types";
import {
buildPagination,
generatePatientPID,
handlePrismaError,
} from "@/common/utils/functions";
import {prisma} from "@/common/lib/prisma";
import {ServerResponse} from "@/common/types";
import {userRequested} from "@/modules/users/types";
class PatientServiceClass extends Controller {
async getAllPatientWithoutPagination(req: any) {
try {
return await prisma.patient.findMany();
} catch (error) {
handlePrismaError(error);
}
}
async getAllPatients(req: any, res: ServerResponse, user: userRequested) {
const page = req?.query?.page;
const limit = req?.query?.limit;
const name = req?.query?.name;
const lastname = req?.query?.lastname;
const ncode = req?.query?.ncode;
const pcode = req?.query?.pcode;
const phone = req?.query?.phone;
const email = req?.query?.email;
const age = req?.query?.age;
const is_deleted = req?.query?.is_deleted;
const skip = (page - 1) * limit;
const id = req?.query?.id;
const isAdmin =
user.role === "developer" || user.role === "admin" ? true : false;
const where: any = {
deletedAt: is_deleted === "true" ? {not: null} : null,
...(name && {
firstName: {contains: name, mode: "insensitive"},
}),
...(lastname && {
lastName: {contains: lastname, mode: "insensitive"},
}),
...(ncode && {nationalityCode: ncode}),
...(pcode && {passportCode: pcode}),
...(phone && {phone}),
...(email && {email}),
...(age && {age: Number(age)}),
...(id && {pid: id}),
};
try {
const [data, total] = await Promise.all([
prisma.patient.findMany({
skip,
take: +limit,
orderBy: {createdAt: "desc"},
where,
select: {
id: true,
pid: true,
firstName: true,
lastName: true,
nationality: {
select: {
id: true,
name: true,
callCode: true,
},
},
nationalityCode: true,
passportCode: true,
preferredLanguage: true,
phone: true,
email: true,
address: true,
birthDate: true,
sex: true,
postalCode: true,
createdAt: true,
documents: isAdmin
? {
select: {
fileUrl: true,
status: true,
type: true,
case: true,
uploadedBy: true,
},
}
: false,
},
}),
await prisma.patient.count({
where,
}),
]);
return {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
data,
};
} catch (error) {
handlePrismaError(error);
}
}
async getSinglePatient(id: string) {
const patient = await this.isExistPatient(id);
return patient;
}
async createPatient(data: createPatientDataBody) {
const pid = await generatePatientPID();
try {
await prisma.patient.create({
data: {
pid,
firstName: data.firstName,
lastName: data.lastName,
email: data?.email,
phone: data?.phone,
nationalityId: +data.nationality!,
preferredLanguage: data?.preferredLanguage,
birthDate: data?.birthDate,
nationalityCode: data?.nationalityCode,
passportCode: data?.passportCode,
sex: data.sex,
deletedAt: null,
address: data.address,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async updatePatient(data: createPatientDataBody, id: string) {
const patient = await this.isExistPatient(id);
try {
await prisma.patient.update({
where: {
id: patient?.id,
deletedAt: null,
},
data: {
firstName: data.firstName,
lastName: data.lastName,
email: data?.email,
phone: data?.phone,
nationalityId: +data?.nationality!,
preferredLanguage: data?.preferredLanguage,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async deletePatient(id: string) {
const patient = await this.isExistPatient(id);
try {
await prisma.patient.update({
where: {
id: patient?.id,
},
data: {
deletedAt: new Date(),
},
});
} catch (error) {
handlePrismaError(error);
}
}
async restorePatient(id: string) {
const patient = await this.isExistPatient(id);
try {
await prisma.patient.update({
where: {
id: patient?.id,
},
data: {
deletedAt: null,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async isExistPatient(id: string) {
let patient;
try {
patient = await prisma.patient.findUnique({where: {id}});
return patient;
} catch (error) {
handlePrismaError(error);
}
}
}
const PatientService = new PatientServiceClass();
export default PatientService;

View File

@@ -0,0 +1,16 @@
import { Sex } from "@/generated/prisma/enums";
export interface createPatientDataBody {
firstName: string;
lastName: string;
nationality?: string;
phone?: string;
email?: string;
preferredLanguage?: string;
age?: number;
address?:string
sex?:Sex
birthDate?:Date
nationalityCode?:string,
passportCode?:string
}

View File

@@ -0,0 +1,70 @@
import Joi from "joi";
export const CreatePatientValidationSchema = Joi.object({
firstName: Joi.string().trim().required().messages({
"any.required": "نام بیمار الزامیست",
"string.empty": "نام بیمار نمی تواند خالی باشد",
"string.base": "نام بیمار نامعتبر است",
}),
lastName: Joi.string().trim().required().messages({
"any.required": "نام خانوادگی بیمار الزامیست",
"string.empty": "نام خانوادگی بیمار نمی تواند خالی باشد",
"string.base": "نام خانوادگی بیمار نامعتبر است",
}),
nationality: Joi.string().trim().required().messages({
"any.required": "ملیت بیمار وارد نشده است",
"string.empty": "ملیت بیمار نمیتواند خالی باشد",
"string.base": "ملیت بیمار فرمت اشتباهی دارد",
}),
sex: Joi.string().valid("male", "female", "other").required().messages({
"any.only": "جنسیت بیمار نامعتبر است",
"any.required": "جنسیت بیمار وارد نشده است",
}),
phone: Joi.string().max(20).allow("").optional().messages({
"string.base": "شماره تماس نامعتبر است",
"string.max": "شماره تماس بیشتر از حد مجاز است",
"string.empty":"شماره تماس نمیتواند خالی باشد"
}),
email: Joi.string().email().allow("").max(255).optional().messages({
"string.email": "ایمیل نامعتبر است",
"string.max": "ایمیل نباید بیشتر از 255 کاراکتر باشد",
}),
address: Joi.string().optional().allow(""),
preferredLanguage: Joi.string().optional().allow("en","fa","ar").messages({
"string.base": "زبان ترجیحی معتبر نیست",
"any.only":" تنها زبان های فارسی ، انگلیسی و عربی مجاز است"
}),
birthDate: Joi.string().isoDate().optional().messages({
"string.isoDate": "تاریخ تولد باید به فرمت ISO معتبر باشد",
}),
age: Joi.number().integer().min(0).max(150).optional().messages({
"number.base": "سن باید عدد باشد",
"number.integer": "سن باید عدد صحیح باشد",
"number.min": "سن نمی‌تواند کمتر از ۰ باشد",
"number.max": "سن نمی‌تواند بیشتر از 150 باشد",
}),
// فرض منطقی بر اساس متن خطا
nationalCode: Joi.string().optional().allow(""),
passportCode: Joi.string().optional().allow(""),
})
// --- phone OR email ---
.or("phone", "email")
.messages({
"object.missing": "وارد کردن تلفن یا ایمیل الزامی است",
})
// --- nationalCode OR passportCode ---
.or("nationalCode", "passportCode")
.messages({
"object.missing": "وارد کردن کد ملی یا کد پاسپورت الزامیست",
});

View File

@@ -0,0 +1,39 @@
import {Controller} from "@/core/controller/main.controller";
import PrivacyPolicyService from "../service/privacy-policy.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
class PrivacyPolicyControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = PrivacyPolicyService;
}
async update(req: any, res: ServerResponse, next: NextFunction) {
try {
await this.#service.update(req.body);
return res.status(200).json({
status: 200,
data: {},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async get(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.get();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
}
const PrivacyPolicyController = new PrivacyPolicyControllerClass();
export default PrivacyPolicyController;

View File

@@ -0,0 +1,8 @@
import express from 'express'
import PrivacyPolicyController from '../controller/privacy-policy.controller';
const privacy_policy_router = express.Router();
privacy_policy_router.put('/update',PrivacyPolicyController.update)
privacy_policy_router.get('/get',PrivacyPolicyController.get)
export default privacy_policy_router;

View File

@@ -0,0 +1,44 @@
import {prisma} from "@/common/lib/prisma";
import {handlePrismaError} from "@/common/utils/functions";
import {Controller} from "@/core/controller/main.controller";
import {PrivacyPolicy} from "@/generated/prisma/client";
class PrivacyPolicyServiceClass extends Controller {
async update(data: PrivacyPolicy) {
// let target = null;
// try {
// target = await prisma.privacyPolicy.findUnique({where: {id}});
// } catch (error) {
// handlePrismaError(error);
// }
try {
await prisma.privacyPolicy.upsert({
where: {id: 1},
create: {
content: data.content,
},
update: {
content: data.content,
},
});
} catch (error) {
handlePrismaError(error);
}
}
async get() {
try {
const data = await prisma.privacyPolicy.findUnique({
where: {id: 1},
});
return data;
} catch (error) {
console.log(error)
handlePrismaError(error);
}
}
}
const PrivacyPolicyService = new PrivacyPolicyServiceClass();
export default PrivacyPolicyService;

View File

@@ -0,0 +1,119 @@
import { Controller } from "@/core/controller/main.controller";
import PublicApisService from "./index.service";
import { ServerResponse } from "@/common/types";
import { NextFunction } from "express";
class PublicApisControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = PublicApisService;
}
async getTopNavBarData(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getTopNavBarData();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getAboutUsText(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAboutUsText();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getDepartmentMemberProfiles(
req: any,
res: ServerResponse,
next: NextFunction,
) {
try {
const data = await this.#service.getDepartmentMemberProfiles();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getHumanRights(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getHumanRights();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getContactUs(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getContactUs();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getFooter(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getFooter();
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getPricing(req: any, res: ServerResponse, next: NextFunction) {
try {
const langId = req.langId;
const data = await this.#service.getPricing(langId);
return res.status(200).json({
status: 200,
data,
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getMedicalPackagesParents(req:any,res:ServerResponse,next:NextFunction){
try {
const data = await this.#service.getMedicalPackagesParents();
} catch (error) {
next(error)
}
}
}
const PublicApisController = new PublicApisControllerClass();
export default PublicApisController;

View File

@@ -0,0 +1,26 @@
import express from "express";
import PublicApisController from "./index.controller";
import { checkLanguageSlug } from "@/core/middlewares/check-language.middleware";
const publicApisRouter = express.Router();
publicApisRouter.get("/top-navbar", PublicApisController.getTopNavBarData);
publicApisRouter.get("/get/about-us-text", PublicApisController.getAboutUsText);
publicApisRouter.get(
"/get/department-members",
PublicApisController.getDepartmentMemberProfiles,
);
publicApisRouter.get("/get/human-rights", PublicApisController.getHumanRights);
publicApisRouter.get("/get/contact-us", PublicApisController.getContactUs);
publicApisRouter.get("/get/footer", PublicApisController.getFooter);
publicApisRouter.get(
"/get/pricing/:lang",
checkLanguageSlug,
PublicApisController.getPricing,
);
publicApisRouter.get(
"/medical-packages-parents",
PublicApisController.getMedicalPackagesParents,
);
export default publicApisRouter;

View File

@@ -0,0 +1,301 @@
import { prisma } from "@/common/lib/prisma";
import { handlePrismaError } from "@/common/utils/functions";
import { Controller } from "@/core/controller/main.controller";
interface TopnavbarDataType {
email: string;
instagram: string;
languages: {
title: string;
slug: string;
}[];
}
class PublicApisServiceClass extends Controller {
async getTopNavBarData() {
let data: TopnavbarDataType = {
email: "",
instagram: "",
languages: [],
};
try {
const result = await prisma.default.findFirst({
select: {
email: true,
instagramLink: true,
},
});
data.email = result?.email ?? "";
data.instagram = result?.instagramLink ?? "";
} catch (error) {
return true;
}
try {
const result = await prisma.language.findMany({
select: {
title: true,
slug: true,
},
});
data.languages = result;
} catch (error) {
return true;
}
return data;
}
async getAboutUsText() {
try {
const data = await prisma.default.findFirst({
select: {
translations: {
select: {
aboutUsText: true,
language: {
select: {
slug: true,
},
},
},
},
},
});
return data;
} catch (error) {
return null;
}
}
async getDepartmentMemberProfiles() {
try {
const data = await prisma.users.findMany({
take: 3,
where: {
OR: [
{
type: "DEPARTMENT",
}
],
},
select: {
translations: {
select: {
excerpt: true,
firstName: true,
lastName: true,
position: true,
lang: {
select: {
slug: true,
},
},
},
},
image:{
select:{
fileUrl:true
}
}
},
});
return data;
} catch (error) {
console.log(error)
return null;
}
}
async getHumanRights() {
try {
const data = await prisma.default.findFirst({
select: {
translations: {
select: {
patientsRights: true,
language: {
select: {
slug: true,
title: true,
},
},
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getContactUs() {
try {
const data = await prisma.$transaction([
prisma.default.findFirst({
select: {
email: true,
hospitalPhone: true,
mapAddress: true,
instagramLink: true,
ipdNumber: true,
translations: {
select: {
address: true,
language: {
select: {
slug: true,
title: true,
},
},
},
},
},
}),
prisma.staff.findFirst({
where: {
role: "coordinator",
status: "ACTIVE",
is_verified: true,
},
select: {
email: true,
translations: {
select: {
displayName: true,
position: true,
lang: {
select: {
slug: true,
},
},
},
},
profilePicture: {
select: {
fileUrl: true,
},
},
},
}),
]);
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getFooter() {
try {
const data = await prisma.default.findFirst({
select: {
email: true,
hospitalPhone: true,
mapAddress: true,
instagramLink: true,
ipdNumber: true,
translations: {
select: {
address: true,
underLogoText: true,
language: {
select: {
slug: true,
title: true,
},
},
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getPricing(langId?: number) {
const packages = await prisma.medicalPackage.findMany({
select: {
translations: langId
? {
where: { lang_id: langId },
select: {
lang: { select: { slug: true, title: true } },
title: true,
},
}
: {
select: {
lang: { select: { slug: true, title: true } },
title: true,
},
},
icon: true,
price: true,
priority: true,
id: true,
parent_id: true,
parent: {
select: {
translations: {
select: {
title: true,
lang: {
select: {
slug: true,
title: true,
},
},
},
},
},
},
},
orderBy: {
priority: "asc",
},
});
const map = new Map();
// ساخت map اولیه
packages.forEach((pkg) => {
map.set(pkg.id, { ...pkg, children: [] });
});
const roots: any[] = [];
// ساخت درخت
map.forEach((pkg) => {
if (pkg.parent_id) {
const parent = map.get(pkg.parent_id);
parent?.children.push(pkg);
} else {
roots.push(pkg);
}
});
return roots;
}
async getMedicalPackagesParents() {
try {
const data = await prisma.medicalPackage.findMany({
where: {
parent_id: null,
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
}
const PublicApisService = new PublicApisServiceClass();
export default PublicApisService;

View File

@@ -0,0 +1,91 @@
import {Controller} from "@/core/controller/main.controller";
import ReviewService from "../services/review.service";
import {ServerResponse} from "@/common/types";
import {NextFunction} from "express";
import {createReviewValidationSchema} from "../validation";
import { userRequested } from "@/modules/users/types";
class ReviewControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = ReviewService;
}
async getAllReviews(req:any,res:ServerResponse,next:NextFunction){
try {
await this.#service.getAllReviews();
return res.status(200).json({
status:200,
data:{},
message:"Ok"
})
} catch (error) {
next(error)
}
}
async getSingleReviews(req:any,res:ServerResponse,next:NextFunction){
try {
const reviewId = req?.params?.id;
await this.#service.getSingleReview(reviewId);
return res.status(200).json({
status:200,
data:{},
message:"Ok"
})
} catch (error) {
next(error)
}
}
async createReview(req: any, res: ServerResponse, next: NextFunction) {
try {
const doctor:userRequested = req?.user;
await createReviewValidationSchema.validateAsync(req.body || {}, {
abortEarly: true,
stripUnknown: true,
});
await this.#service.createReview(req.body || {},doctor);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ثبت شد",
});
} catch (error) {
next(error);
}
}
async updateReview(req: any, res: ServerResponse, next: NextFunction) {
try {
const reviewId = req?.params?.id;
await createReviewValidationSchema.validateAsync(req.body || {}, {
abortEarly: true,
stripUnknown: true,
});
const data = await this.#service.updateReview(req.body || {}, reviewId);
return res.status(200).json({
status: 200,
data:{...data},
message: "با موفقیت ویرایش شد",
});
} catch (error) {
next(error);
}
}
async deleteReview(req: any, res: ServerResponse, next: NextFunction) {
try {
const reviewId = req?.params?.id;
await this.#service.deleteReview(reviewId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت حذف شد",
});
} catch (error) {
next(error);
}
}
}
const ReviewController = new ReviewControllerClass();
export default ReviewController;

View File

@@ -0,0 +1,10 @@
import express from 'express'
import ReviewController from '../controller/review.controller';
const review_router = express.Router()
review_router.post('/create',ReviewController.createReview)
review_router.put('/update/:id',ReviewController.updateReview)
review_router.delete('/delete/:id',ReviewController.deleteReview)
export default review_router;

View File

@@ -0,0 +1,205 @@
import {Controller} from "@/core/controller/main.controller";
import {CreateReviewDataBody} from "../types";
import {prisma} from "@/common/lib/prisma";
import createHttpError from "http-errors";
import {handlePrismaError} from "@/common/utils/functions";
import {userRequested} from "@/modules/users/types";
class ReviewServiceClass extends Controller {
async getAllReviews() {
try {
const data = await prisma.review.findMany({
include: {
translations: {
select: {
lang: true,
note: true,
result: true,
id: true,
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async getSingleReview(reviewId: string) {
try {
const data = await prisma.review.findUnique({
where: {id: reviewId},
include: {
translations: {
select: {
lang: true,
note: true,
result: true,
id: true,
},
},
},
});
return data;
} catch (error) {
handlePrismaError(error);
}
}
async createReview(data: CreateReviewDataBody, user: userRequested) {
const onlineCase = await this.isOnlineCaseExist(data.caseId);
const doctor = await this.isDoctorExist(data.doctorId);
try {
await prisma.review.create({
data: {
caseId: onlineCase.id,
doctorId: doctor.id,
translations: {
create: data.translations.map((t) => ({
lang_id: t.lang_id,
note: t.note ?? null,
result: t.result ?? null,
})),
},
},
});
} catch (error) {
handlePrismaError(error);
}
await this.updateCaseStatus(onlineCase.id, "PRE_APPROVED", doctor.id);
}
async updateReview(data: CreateReviewDataBody, reviewId: string) {
await this.isOnlineCaseExist(data.caseId);
await this.isDoctorExist(data.doctorId);
const {translations, ...rest} = data;
try {
const existingReview = await prisma.review.findUnique({
where: {id: reviewId},
include: {translations: true},
});
if (!existingReview) {
throw new createHttpError.NotFound("Review not found");
}
const existingLangIds = existingReview.translations
.map((t) => t.lang_id)
.filter((id): id is number => id !== null);
const newLangIds = translations?.map((t) => t.lang_id) ?? [];
// زبان‌هایی که باید حذف شوند
const deleteLangIds = existingLangIds.filter(
(langId) => !newLangIds.includes(langId)
);
const updatedReview = await prisma.review.update({
where: {id: reviewId},
data: {
...rest,
translations: {
// حذف ترجمه‌های حذف‌شده
deleteMany: deleteLangIds.map((lang_id) => ({
lang_id,
})),
// upsert ترجمه‌ها
upsert: translations?.map((t) => ({
where: {
reviewId_lang_id: {
reviewId,
lang_id: t.lang_id,
},
},
update: {
note: t.note ?? null,
result: t.result ?? null,
},
create: {
lang_id: t.lang_id,
note: t.note ?? null,
result: t.result ?? null,
},
})),
},
},
include: {
translations: true,
},
});
return updatedReview;
} catch (error) {
handlePrismaError(error);
}
}
async deleteReview(reviewId: string) {
try {
await prisma.review.update({
where: {
id: reviewId,
},
data: {
deletedAt: Date.now().toString(),
},
});
} catch (error) {
handlePrismaError(error);
}
}
async permanentDeleteReview(reviewId: string) {
try {
await prisma.reviewTranslation.deleteMany({
where: {reviewId},
});
await prisma.review.delete({
where: {id: reviewId},
});
} catch (error) {
handlePrismaError(error);
}
}
async isOnlineCaseExist(id: string) {
const OnlineCase = await prisma.onlineCase.findUnique({
where: {id},
});
if (!OnlineCase) {
throw new createHttpError.NotFound("پرونده بیمار یافت نشد");
}
return OnlineCase;
}
async isReviewExist(id: string) {
const review = await prisma.review.findUnique({
where: {id},
});
if (!review) {
throw new createHttpError.NotFound("پرونده بیمار یافت نشد");
}
return review;
}
async isDoctorExist(id: string) {
const doctor = await prisma.staff.findUnique({
where: {id, role: "doctor"},
});
if (!doctor) {
throw new createHttpError.NotFound("شناسه دکتر یافت نشد");
}
return doctor;
}
}
const ReviewService = new ReviewServiceClass();
export default ReviewService;

View File

@@ -0,0 +1,22 @@
export interface CreateReviewTranslationsType {
note?:string,
result?:string,
lang_id:number
}
export interface CreateReviewDataBody {
caseId:string,
doctorId:string,
note?:string
result?:string,
translations:CreateReviewTranslationsType[]
}
export interface UpdateReviewBody {
doctorId?: string | null;
translations?: {
lang: string;
note?: string;
result?: string;
}[];
}

View File

@@ -0,0 +1,33 @@
import Joi from "joi";
const translationSchema = Joi.object({
lang: Joi.string().length(2).required().messages({
"string.base": "Language must be a string",
"string.length": "Language code must be 2 characters",
"any.required": "Language is required",
}),
note: Joi.string().required().messages({
"string.base": "note must be a string",
"string.empty": "note cannot be empty",
"any.required": "note is required",
}),
result: Joi.string().required().messages({
"string.base": "result must be a string",
"string.empty": "result cannot be empty",
"any.required": "result is required",
}),
});
export const createReviewValidationSchema = Joi.object({
caseId: Joi.string().required(),
doctorId: Joi.string().required(),
translations: Joi.array()
.items(translationSchema)
.min(1)
.required()
.messages({
"array.base": "Translations must be an array",
"array.min": "At least one translation is required",
"any.required": "Translations are required",
}),
});

View File

@@ -0,0 +1,174 @@
import {Controller} from "@/core/controller/main.controller";
import StaffService from "../service/staff.service";
import {
ServerResponse
} from "@/common/types";
import {NextFunction} from "express";
import {CreateStaffValidationSchema} from "../validation/staff.validation";
import { CreateStaffBodyData } from "../types";
class StaffControllerClass extends Controller {
#service;
constructor() {
super();
this.#service = StaffService;
}
async getAllStaffWithoutPagination(req: any, res: ServerResponse, next: NextFunction) {
try {
const data = await this.#service.getAllStaffWithoutPagination();
return res.status(200).json({
status: 200,
data: {...data},
message: "Ok",
});
} catch (error) {
next(error);
}
}
async getAllStaff(req: any, res: ServerResponse, next: NextFunction) {
try {
const staff = await this.#service.getAllStaff(req,res);
return res.status(200).json({
status: 200,
data: {...staff},
message: "",
});
} catch (error) {
next(error);
}
}
async getSingleStaff(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id
const staff = await this.#service.getSingleStaff(targetId);
return res.status(200).json({
status: 200,
data: {...staff},
message: "",
});
} catch (error) {
next(error);
}
}
async createStaff(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreateStaffValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
const {
username,
password,
email,
role,
is_verified,
send_notif,
translations
}: CreateStaffBodyData = req.body;
await this.#service.createStaff({
username,
password,
email,
role,
is_verified,
send_notif,
translations
});
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ایجاد شد",
});
} catch (error) {
next(error);
}
}
async updateStaff(req: any, res: ServerResponse, next: NextFunction) {
try {
await CreateStaffValidationSchema.validateAsync(req.body || {}, {
stripUnknown: true,
abortEarly: true,
});
const {
username,
password,
email,
role,
is_verified,
send_notif,
translations
}: CreateStaffBodyData = req.body;
const targetUserId = req?.params?.id;
await this.#service.updateStaff(
{
username,
password,
email,
role,
is_verified,
send_notif,
translations
},
targetUserId
);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت ایجاد شد",
});
} catch (error) {
next(error);
}
}
async deleteStaff(req: any, res: ServerResponse, next: NextFunction) {
try {
const targetId = req?.params?.id;
await this.#service.deleteStaff(targetId);
return res.status(200).json({
status: 200,
data: {},
message: "با موفقیت حذف گردید",
});
} catch (error) {
next(error);
}
}
// async getDepartmentMembers(req:any,res:ServerResponse,next:NextFunction){
// try {
// const data = await this.#service.getDepartmentMembers(req);
// return res.status(200).json({
// status:200,
// data,
// message:"Ok"
// })
// } catch (error) {
// next(error)
// }
// }
async createDeveloperAccount(req:any,res:ServerResponse,next:NextFunction){
try {
await this.#service.createDeveloperAccount();
return res.status(200).json({
status:200,
data:{},
message:"Created"
})
} catch (error) {
next(error)
}
}
}
const StaffController = new StaffControllerClass();
export default StaffController;

View File

@@ -0,0 +1,25 @@
import {authMiddleware} from "@/core/middlewares/auth.middleware";
import {role_authorize} from "@/core/middlewares/role-authorize.middleware";
import express from "express";
import StaffController from "../controller/staff.controller";
const staff_router = express.Router();
staff_router.post("/create", StaffController.createStaff);
staff_router.put(
"/update/:id",
authMiddleware,
role_authorize("developer", "admin"),
StaffController.updateStaff
);
staff_router.delete(
"/delete/:id",
authMiddleware,
role_authorize("developer", "admin"),
StaffController.deleteStaff
);
staff_router.get("/get/all", StaffController.getAllStaff);
staff_router.get('/create-developer-account',StaffController.createDeveloperAccount)
// staff_router.get('/department/members',StaffController.getDepartmentMembers)
export default staff_router;

View File

@@ -0,0 +1,293 @@
import {SELECT_STAFF_OUT_DATA} from "@/common/constants/variables";
import {prisma} from "@/common/lib/prisma";
import {HashPassword} from "@/common/utils/generate";
import {Controller} from "@/core/controller/main.controller";
import createHttpError from "http-errors";
import {CreateStaffBodyData} from "../types";
import {buildPagination, handlePrismaError} from "@/common/utils/functions";
import {ServerResponse} from "@/common/types";
class StaffServiceClass extends Controller {
async getAllStaffWithoutPagination() {
try {
return await prisma.staff.findMany();
} catch (error) {
handlePrismaError(error);
}
}
async getAllStaff(req: any, res: ServerResponse) {
return await buildPagination(req, res, "staff", {
include: {
translations: {
select: {
displayName: true,
position: true,
description: true,
lang: true,
},
},
},
});
}
async getSingleStaff(id: string) {
try {
return await prisma.staff.findUnique({
where: {
id,
},
select: {
...SELECT_STAFF_OUT_DATA,
translations: {
select: {
displayName: true,
position: true,
description: true,
lang: true,
},
},
},
});
} catch (error) {
throw new createHttpError.InternalServerError("خطایی رخ داده است");
}
}
async createStaff(data: CreateStaffBodyData) {
const staff = await this.isStaffExist(
data.username,
data.email || undefined
);
if (staff) {
throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است");
}
const hashedPassword = await HashPassword(data.password);
try {
await prisma.staff.create({
data: {
username: data.username.trim(),
email: data.email,
password: hashedPassword,
role: data.role || "coordinator",
is_verified: data.is_verified,
send_notif_with_email: data.send_notif,
translations: {
create: data?.translations?.map((t) => ({
lang_id: t.lang_id,
displayName: t.displayName ?? null,
position: t.position ?? null,
description: t.description ?? null,
})),
},
},
include: {translations: true},
});
} catch (error) {
console.log(error)
handlePrismaError(error);
}
return true;
}
async updateStaff(data: CreateStaffBodyData, staffId: string) {
const staff = await this.findStaffById(staffId);
const {translations, ...rest} = data;
if (staff.username !== data.username) {
const findAlreadyStaff = await this.isStaffUsernameExist(data.username);
if (findAlreadyStaff) {
throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است");
}
}
if (staff.email !== data.email) {
const findAlreadyStaff = await this.isStaffEmailExist(data.email);
if (findAlreadyStaff) {
throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است");
}
}
try {
const existingStaff = await prisma.staff.findUnique({
where: {id: staffId},
include: {translations: true},
});
if (!existingStaff) {
throw new createHttpError.NotFound("Staff not found");
}
// lang_id های موجود
const existingLangIds = existingStaff.translations
.map((t) => t.lang_id)
.filter((id): id is number => id !== null);
// lang_id های جدید
const newLangIds = translations?.map((t) => t.lang_id) ?? [];
// ترجمه‌هایی که باید حذف شوند
const deleteLangIds = existingLangIds.filter(
(langId) => !newLangIds.includes(langId)
);
const updatedStaff = await prisma.staff.update({
where: {id: staffId},
data: {
// فیلدهای اصلی Staff
...rest, // username, email, role, is_verified, ...
translations: {
// حذف ترجمه‌های حذف‌شده
deleteMany: deleteLangIds.map((lang_id) => ({
lang_id,
})),
// upsert ترجمه‌ها
upsert: translations?.map((t) => ({
where: {
staffId_lang_id: {
staffId,
lang_id: t.lang_id,
},
},
update: {
displayName: t.displayName ?? null,
position: t.position ?? null,
description: t.description ?? null,
},
create: {
lang_id: t.lang_id,
displayName: t.displayName ?? null,
position: t.position ?? null,
description: t.description ?? null,
},
})),
},
},
include: {
translations: true,
},
});
return updatedStaff;
} catch (error) {
handlePrismaError(error);
}
return true;
}
async deleteStaff(id: string) {
await this.findStaffById(id);
try {
await prisma.staffTranslation.deleteMany({
where: {staffId: id},
});
await prisma.staff.delete({
where: {
id,
},
});
} catch (error) {
throw new createHttpError.InternalServerError("خطایی رخ داده است");
}
}
async isStaffExist(username: string, email: string | undefined) {
try {
const staff = await prisma.staff.findUnique({
where: {
username,
email,
},
});
if (staff) {
return true;
}
return false;
} catch (error) {
throw new createHttpError.InternalServerError("خطا در جستجوی کاربر");
}
}
async isStaffUsernameExist(username: string) {
try {
const staff = await prisma.staff.findUnique({
where: {
username,
},
});
if (staff) {
return true;
}
return false;
} catch (error) {
throw new createHttpError.InternalServerError("خطا در جستجوی کاربر");
}
}
async isStaffEmailExist(email: string) {
try {
const staff = await prisma.staff.findUnique({
where: {
email,
},
});
if (staff) {
return true;
}
return false;
} catch (error) {
throw new createHttpError.InternalServerError("خطا در جستجوی کاربر");
}
}
async findStaffById(id: string) {
try {
const staff = await prisma.staff.findUnique({
where: {
id,
},
select: {
id: true,
username: true,
email: true,
translations: true,
},
});
if (!staff) {
throw new createHttpError.NotFound("کاربر یافت نشد");
}
return staff;
} catch (error) {
throw new createHttpError.InternalServerError("خطا در جستجوی کاربر");
}
}
async createDeveloperAccount(){
const hashedPassword = await HashPassword("Moji1234");
try {
await prisma.staff.create({
data:{
username:"romiz",
password:hashedPassword,
role:"developer",
status:"ACTIVE",
is_verified:true,
email:"test@test.com"
}
})
return true;
} catch (error) {
handlePrismaError(error)
}
}
}
const StaffService = new StaffServiceClass();
export default StaffService;

View File

@@ -0,0 +1,17 @@
import {StaffRoles} from "@/generated/prisma/enums";
export interface CreateStaffTranslationsType {
lang_id: number;
displayName?: string;
position?: string;
description?: string;
}
[];
export interface CreateStaffBodyData {
username: string;
password: string;
email: string;
role: StaffRoles;
is_verified: boolean;
send_notif: boolean;
translations: CreateStaffTranslationsType[];
}

View File

@@ -0,0 +1,41 @@
import {StaffRoles} from "@/generated/prisma/enums";
import Joi from "joi";
export const CreateStaffValidationSchema = Joi.object({
username: Joi.string()
.required()
.min(4)
.pattern(/^[a-zA-Z0-9._]{3,30}$/)
.messages({
"string.base": "نام کاربری باید یک رشته متنی باشد.",
"string.empty": "نام کاربری نمی‌تواند خالی باشد.",
"string.pattern.base":
"فرمت نام کاربری معتبر نیست. فقط حروف انگلیسی، عدد، نقطه و آندرلاین مجاز است و باید بین ۳ تا ۳۰ کاراکتر باشد.",
"any.required": "وارد کردن نام کاربری الزامی است.",
}),
password: Joi.string()
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$/)
.messages({
"string.base": "پسورد باید یک رشته متنی باشد.",
"string.empty": "پسورد نمی‌تواند خالی باشد.",
"string.pattern.base":
"پسورد باید حداقل ۸ کاراکتر باشد و حداقل یک حرف کوچک، یک حرف بزرگ و یک عدد داشته باشد.",
"any.required": "وارد کردن پسورد الزامی است.",
}),
email: Joi.string()
.email({tlds: {allow: false}})
.messages({
"string.base": "ایمیل باید یک رشته متنی باشد.",
"string.empty": "ایمیل نمی‌تواند خالی باشد.",
"string.email": "فرمت ایمیل معتبر نیست.",
"any.required": "وارد کردن ایمیل الزامی است.",
}),
role: Joi.valid("developer","admin","coordinator","doctor")
.required()
.messages({
"any.only": "نقش معتبر نیست",
"any.required": "انتخاب نقش الزامی است.",
}),
is_verified: Joi.boolean().optional(),
send_notif: Joi.boolean().optional(),
});

Some files were not shown because too many files have changed in this diff Show More