Compare commits

..

5 Commits

Author SHA1 Message Date
kailasdevdas daf0178330 Merge pull request '[1.0.4]' (#29) from dev into main
Reviewed-on: #29
2026-05-14 03:53:43 +00:00
kailasdevdas 7e39683aa2 Merge pull request '[1.0.3]' (#25) from dev into main
Reviewed-on: #25
2026-05-12 05:38:48 +00:00
ashir f60dbe14e6 Merge pull request '1.0.2' (#22) from dev into main
Reviewed-on: #22
2026-05-05 12:29:11 +00:00
ashir 71da0243be Merge pull request '[1.0.1]' (#20) from dev into main
Reviewed-on: #20
2026-04-30 19:20:58 +00:00
ashir 86d41f6c2f Merge pull request '[1.0.0]' (#19) from dev into main
Reviewed-on: #19
2026-04-30 18:37:18 +00:00
149 changed files with 13315 additions and 18340 deletions
+1 -2
View File
@@ -1,2 +1 @@
.env .env
node_modules
-14
View File
@@ -1,14 +0,0 @@
node_modules
dist
build
coverage
.next
out
*.log
backend/node_modules
backend/dist
frontend/node_modules
frontend/build
frontend/dist
-13
View File
@@ -1,13 +0,0 @@
{
"printWidth": 120,
"useTabs": true,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"bracketSameLine": false,
"endOfLine": "lf"
}
-4
View File
@@ -1,4 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
-3
View File
@@ -35,7 +35,6 @@ npm run create-user <username> <password> [role] # role defaults to "admin"
``` ```
In Docker, create an admin via: In Docker, create an admin via:
```bash ```bash
docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role> docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role>
``` ```
@@ -87,7 +86,6 @@ Note the natural-key relations: `Appointment.doctorId` references `Doctor.doctor
## Environment variables ## Environment variables
`backend/.env`: `backend/.env`:
``` ```
DATABASE_URL=postgresql://user:password@db:5432/mydb DATABASE_URL=postgresql://user:password@db:5432/mydb
PORT=5008 PORT=5008
@@ -99,7 +97,6 @@ EMAIL_FROM=admin@example.com
``` ```
`frontend/.env`: `frontend/.env`:
``` ```
VITE_API_URL=http://localhost:5008/api VITE_API_URL=http://localhost:5008/api
``` ```
+5 -5
View File
@@ -1,14 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following: // This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv // npm install --save-dev prisma dotenv
import 'dotenv/config'; import "dotenv/config";
import { defineConfig } from 'prisma/config'; import {defineConfig} from "prisma/config";
export default defineConfig({ export default defineConfig({
schema: 'prisma/schema.prisma', schema: "prisma/schema.prisma",
migrations: { migrations: {
path: 'prisma/migrations', path: "prisma/migrations",
}, },
datasource: { datasource: {
url: process.env['DATABASE_URL'], url: process.env["DATABASE_URL"],
}, },
}); });
@@ -1,63 +0,0 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthCheckCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2),
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"categoryId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackageInquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"preferredDate" TIMESTAMP(3),
"message" TEXT,
"packageId" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackageInquiry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_name_key" ON "HealthCheckCategory"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_slug_key" ON "HealthCheckCategory"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_slug_key" ON "HealthPackage"("slug");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "HealthCheckCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HealthPackageInquiry" ADD CONSTRAINT "HealthPackageInquiry_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "HealthPackage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
@@ -1,50 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `Doctor` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT,
ADD COLUMN "seoId" INTEGER;
-- CreateTable
CREATE TABLE "DoctorSpecialization" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"doctorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DoctorSpecialization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Seo" (
"id" SERIAL NOT NULL,
"seoTitle" TEXT,
"metaDescription" TEXT,
"focusKeyphrase" TEXT,
"slug" TEXT,
"tags" TEXT[],
"ogTitle" TEXT,
"ogDescription" TEXT,
"ogImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Seo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Seo_slug_key" ON "Seo"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Doctor_seoId_key" ON "Doctor"("seoId");
-- AddForeignKey
ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
@@ -1,14 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `HealthPackage` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "seoId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_seoId_key" ON "HealthPackage"("seoId");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,22 +0,0 @@
-- CreateEnum
CREATE TYPE "BannerMediaType" AS ENUM ('IMAGE', 'VIDEO');
-- CreateTable
CREATE TABLE "HomepageBanner" (
"id" SERIAL NOT NULL,
"title" TEXT,
"subtitle" TEXT,
"mediaType" "BannerMediaType" NOT NULL,
"desktopMediaUrl" TEXT NOT NULL,
"mobileMediaUrl" TEXT,
"buttonText" TEXT,
"buttonLink" TEXT,
"openInNewTab" BOOLEAN NOT NULL DEFAULT false,
"textAlignment" TEXT DEFAULT 'left',
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HomepageBanner_pkey" PRIMARY KEY ("id")
);
@@ -1,13 +0,0 @@
-- CreateTable
CREATE TABLE "InsurancePartner" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"logo" TEXT NOT NULL,
"websiteUrl" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InsurancePartner_pkey" PRIMARY KEY ("id")
);
@@ -1,18 +0,0 @@
-- CreateEnum
CREATE TYPE "AccreditationType" AS ENUM ('ACCREDITATION', 'CERTIFICATION');
-- CreateTable
CREATE TABLE "Accreditation" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"type" "AccreditationType" NOT NULL,
"logo" TEXT,
"image" TEXT,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Accreditation_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false;
+1 -160
View File
@@ -23,16 +23,11 @@ model Doctor {
name String name String
image String? image String?
designation String? designation String?
experience Int?
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true) isActive Boolean @default(true)
isFeatured Boolean @default(false)
globalSortOrder Int @default(1000) globalSortOrder Int @default(1000)
specializations DoctorSpecialization[]
professionalSummary String? @db.Text
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
departments DoctorDepartment[] departments DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
@@ -225,157 +220,3 @@ model NewsImage {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
model HealthCheckCategory {
id Int @id @default(autoincrement())
name String @unique
slug String? @unique
description String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
packages HealthPackage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackage {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal? @db.Decimal(10, 2)
image String?
discountedPrice Decimal? @db.Decimal(10, 2)
inclusions Json @default("{}")
isActive Boolean @default(true)
isFeatured Boolean @default(false)
sortOrder Int @default(1000)
categoryId Int
category HealthCheckCategory @relation(fields: [categoryId], references: [id])
inquiries HealthPackageInquiry[]
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackageInquiry {
id Int @id @default(autoincrement())
fullName String
mobileNumber String
email String?
age Int?
gender String?
preferredDate DateTime?
message String?
packageId Int
healthPackage HealthPackage @relation(fields: [packageId], references: [id])
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorSpecialization {
id Int @id @default(autoincrement())
name String
description String? @db.Text
doctorId Int
doctor Doctor @relation(fields: [doctorId],references: [id],onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Seo {
id Int @id @default(autoincrement())
doctor Doctor?
healthPackage HealthPackage?
seoTitle String?
metaDescription String? @db.Text
focusKeyphrase String?
slug String? @unique
tags String[]
ogTitle String?
ogDescription String? @db.Text
ogImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HomepageBanner {
id Int @id @default(autoincrement())
title String?
subtitle String?
mediaType BannerMediaType
desktopMediaUrl String
mobileMediaUrl String?
buttonText String?
buttonLink String?
openInNewTab Boolean @default(false)
textAlignment String? @default("left")
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum BannerMediaType {
IMAGE
VIDEO
}
model InsurancePartner {
id Int @id @default(autoincrement())
name String
logo String
websiteUrl String?
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Accreditation {
id Int @id @default(autoincrement())
title String
type AccreditationType
logo String?
image String?
description String? @db.Text
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum AccreditationType {
ACCREDITATION
CERTIFICATION
}
+37 -45
View File
@@ -1,68 +1,60 @@
import express from 'express'; import express from "express";
import dotenv from 'dotenv'; import dotenv from "dotenv";
import cors from 'cors'; import cors from "cors";
import departmentRoutes from './routes/department.routes.js'; import departmentRoutes from "./routes/department.routes.js";
import authRoutes from './routes/auth.routes.js'; import authRoutes from "./routes/auth.routes.js";
import blogRoutes from './routes/blog.routes.js'; import blogRoutes from "./routes/blog.routes.js";
import uploadRoutes from './routes/upload.routes.js'; import uploadRoutes from "./routes/upload.routes.js";
import doctorRoutes from './routes/doctor.routes.js'; import doctorRoutes from "./routes/doctor.routes.js";
import careerRoutes from './routes/career.routes.js'; import careerRoutes from "./routes/career.routes.js";
import candidateRoutes from './routes/candidate.routes.js'; import candidateRoutes from "./routes/candidate.routes.js";
import appointmentRoutes from './routes/appointment.routes.js'; import appointmentRoutes from "./routes/appointment.routes.js";
import inquiryRoutes from './routes/inquiry.routes.js'; import inquiryRoutes from "./routes/inquiry.routes.js";
import academicsResearchRoutes from './routes/academicsResearch.routes.js'; import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
import emailConfigRoutes from './routes/emailConfig.routes.js'; import emailConfigRoutes from "./routes/emailConfig.routes.js";
import newsMediaRoutes from './routes/newsMedia.routes.js'; import newsMediaRoutes from "./routes/newsMedia.routes.js";
import importRoutes from './routes/importRoutes.js'; import importRoutes from "./routes/importRoutes.js";
import healthCheckRoutes from './routes/healthCheck.route.js';
import homepageBannerRoutes from './routes/homepageBanner.routes.js';
import insurancePartnerRoutes from './routes/insurancePartner.routes.js';
import accreditationRoutes from './routes/accreditation.routes.js';
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.urlencoded({ limit: "50mb", extended: true }));
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(' ') ? process.env.CORS_ALLOWED_ORIGINS.split(" ")
: ['http://localhost:3001']; : ["http://localhost:3001"];
const corsOptions = { const corsOptions = {
origin: function (origin, callback) { origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) { if (!origin || allowedOrigins.includes(origin)) {
callback(null, true); callback(null, true);
} else { } else {
callback(new Error('Not allowed by CORS')); callback(new Error("Not allowed by CORS"));
} }
}, },
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: '*', allowedHeaders: "*",
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use('/api/departments', departmentRoutes); app.use("/api/departments", departmentRoutes);
app.use('/api/auth', authRoutes); app.use("/api/auth", authRoutes);
app.use('/api/blogs', blogRoutes); app.use("/api/blogs", blogRoutes);
app.use('/uploads', express.static('uploads')); app.use("/uploads", express.static("uploads"));
app.use('/api/upload', uploadRoutes); app.use("/api/upload", uploadRoutes);
app.use('/api/doctors', doctorRoutes); app.use("/api/doctors", doctorRoutes);
app.use('/api/careers', careerRoutes); app.use("/api/careers", careerRoutes);
app.use('/api/candidates', candidateRoutes); app.use("/api/candidates", candidateRoutes);
app.use('/api/appointments', appointmentRoutes); app.use("/api/appointments", appointmentRoutes);
app.use('/api/inquiry', inquiryRoutes); app.use("/api/inquiry", inquiryRoutes);
app.use('/api/academics', academicsResearchRoutes); app.use("/api/academics", academicsResearchRoutes);
app.use('/api/email', emailConfigRoutes); app.use("/api/email", emailConfigRoutes);
app.use('/api/newsMedia', newsMediaRoutes); app.use("/api/newsMedia", newsMediaRoutes);
app.use('/api/import', importRoutes); app.use("/api/import", importRoutes);
app.use('/api/health-check', healthCheckRoutes);
app.use('/api/homepage-banners', homepageBannerRoutes);
app.use('/api/insurance-partners', insurancePartnerRoutes);
app.use('/api/accreditation', accreditationRoutes);
const PORT = process.env.PORT || 5008; const PORT = process.env.PORT || 5008;
app.listen(PORT, () => { app.listen(PORT, () => {
@@ -1,17 +1,18 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js'; import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from '../utils/getEmailByTypes.js'; import { getEmailsByType } from "../utils/getEmailByTypes.js";
// CREATE ACADEMICS & RESEARCH // CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => { export const createAcademicsResearch = async (req, res) => {
try { try {
const { fullName, number, emailId, subject, courseName, message } = req.body; const { fullName, number, emailId, subject, courseName, message } =
req.body;
if (!fullName || !number) { if (!fullName || !number) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Full name and number are required', message: "Full name and number are required",
}); });
} }
@@ -27,12 +28,12 @@ export const createAcademicsResearch = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType('ACADEMICS'); const emailList = await getEmailsByType("ACADEMICS");
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: 'New Academics & Research Inquiry', subject: "New Academics & Research Inquiry",
html: ` html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -62,15 +63,15 @@ export const createAcademicsResearch = async (req, res) => {
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Email:</b></td> <td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${emailId || '-'}</td> <td style="padding: 8px 0;">${emailId || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Course:</b></td> <td style="padding: 8px 0;"><b>Course:</b></td>
<td style="padding: 8px 0;">${courseName || '-'}</td> <td style="padding: 8px 0;">${courseName || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Subject:</b></td> <td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td> <td style="padding: 8px 0;">${subject || "-"}</td>
</tr> </tr>
</table> </table>
@@ -86,7 +87,7 @@ export const createAcademicsResearch = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -104,20 +105,20 @@ export const createAcademicsResearch = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
console.error('Academics email failed:', err); console.error("Academics email failed:", err);
} }
res.status(200).json({ res.status(200).json({
success: true, success: true,
status: 200, status: 200,
data, data,
message: 'Academics & Research added successfully', message: "Academics & Research added successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to add Academics & Research inquiry', message: "Failed to add Academics & Research inquiry",
}); });
} }
}; };
@@ -128,7 +129,7 @@ export const getAcademicsResearch = async (req, res) => {
try { try {
const data = await prisma.academicsResearch.findMany({ const data = await prisma.academicsResearch.findMany({
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -139,7 +140,7 @@ export const getAcademicsResearch = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch records', message: "Failed to fetch records",
}); });
} }
}; };
@@ -159,7 +160,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
if (!data) { if (!data) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Record not found', message: "Record not found",
}); });
} }
@@ -170,7 +171,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch record', message: "Failed to fetch record",
}); });
} }
}; };
@@ -189,12 +190,12 @@ export const deleteAcademicsResearch = async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: 'Record deleted successfully', message: "Record deleted successfully",
}); });
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete record', message: "Failed to delete record",
}); });
} }
}; };
@@ -1,189 +0,0 @@
import prisma from '../prisma/client.js';
export const createAccreditation = async (req, res) => {
try {
const { title, type, logo, image, description, sortOrder, isActive } = req.body;
if (!title || !type) {
return res.status(400).json({
success: false,
message: 'Title and type are required',
});
}
const accreditation = await prisma.accreditation.create({
data: {
title,
type,
logo,
image,
description,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
isActive: isActive ?? true,
},
});
res.status(201).json({
success: true,
data: accreditation,
message: 'Accreditation created successfully',
});
} catch (error) {
console.error('PRISMA TRANSACTION ERROR:', error);
res.status(500).json({
success: false,
message: 'Failed to create accreditation',
});
}
};
export const getAccreditations = async (req, res) => {
try {
const { type } = req.query;
const accreditations = await prisma.accreditation.findMany({
where: type
? {
type,
}
: undefined,
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: accreditations,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditations',
});
}
};
export const getActiveAccreditations = async (req, res) => {
try {
const { type } = req.query;
const accreditations = await prisma.accreditation.findMany({
where: {
isActive: true,
...(type && { type }),
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: accreditations,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditations',
});
}
};
export const getAccreditation = async (req, res) => {
try {
const { id } = req.params;
const accreditation = await prisma.accreditation.findUnique({
where: {
id: Number(id),
},
});
if (!accreditation) {
return res.status(404).json({
success: false,
message: 'Accreditation not found',
});
}
res.json({
success: true,
data: accreditation,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditation',
});
}
};
export const updateAccreditation = async (req, res) => {
try {
const { id } = req.params;
const { title, type, logo, image, description, sortOrder, isActive } = req.body;
const updateData = {};
if (title !== undefined) updateData.title = title;
if (type !== undefined) updateData.type = type;
if (logo !== undefined) updateData.logo = logo;
if (image !== undefined) updateData.image = image;
if (description !== undefined) updateData.description = description;
if (sortOrder !== undefined) updateData.sortOrder = Number(sortOrder);
if (isActive !== undefined) updateData.isActive = isActive;
const accreditation = await prisma.accreditation.update({
where: {
id: Number(id),
},
data: updateData,
});
res.json({
success: true,
data: accreditation,
message: 'Accreditation updated successfully',
});
} catch (error) {
console.error('PRISMA TRANSACTION ERROR:', error);
res.status(500).json({
success: false,
message: 'Failed to update accreditation',
});
}
};
export const deleteAccreditation = async (req, res) => {
try {
const { id } = req.params;
await prisma.accreditation.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Accreditation deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete accreditation',
});
}
};
@@ -1,15 +1,16 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js'; import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from '../utils/getEmailByTypes.js'; import { getEmailsByType } from "../utils/getEmailByTypes.js";
export const createAppointment = async (req, res) => { export const createAppointment = async (req, res) => {
try { try {
const { name, mobileNumber, email, message, date, doctorId, departmentId } = req.body; const { name, mobileNumber, email, message, date, doctorId, departmentId } =
req.body;
if (!name || !mobileNumber || !doctorId || !departmentId || !date) { if (!name || !mobileNumber || !doctorId || !departmentId || !date) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Required fields missing', message: "Required fields missing",
}); });
} }
@@ -30,12 +31,12 @@ export const createAppointment = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType('APPOINTMENT'); const emailList = await getEmailsByType("APPOINTMENT");
if (emailList) { if (emailList) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: 'New Appointment Booked', subject: "New Appointment Booked",
html: ` html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -65,7 +66,7 @@ export const createAppointment = async (req, res) => {
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Email:</b></td> <td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td> <td style="padding: 8px 0;">${email || "-"}</td>
</tr> </tr>
</table> </table>
@@ -74,19 +75,19 @@ export const createAppointment = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
<tr> <tr>
<td style="padding: 8px 0;"><b>Doctor:</b></td> <td style="padding: 8px 0;"><b>Doctor:</b></td>
<td style="padding: 8px 0;">${appointment.doctor?.name || '-'}</td> <td style="padding: 8px 0;">${appointment.doctor?.name || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Department:</b></td> <td style="padding: 8px 0;"><b>Department:</b></td>
<td style="padding: 8px 0;">${appointment.department?.name || '-'}</td> <td style="padding: 8px 0;">${appointment.department?.name || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Date:</b></td> <td style="padding: 8px 0;"><b>Date:</b></td>
<td style="padding: 8px 0;"> <td style="padding: 8px 0;">
${new Date(date).toLocaleDateString('en-GB', { ${new Date(date).toLocaleDateString("en-GB", {
day: '2-digit', day: "2-digit",
month: 'long', month: "long",
year: 'numeric', year: "numeric",
})} })}
</td> </td>
</tr> </tr>
@@ -104,7 +105,7 @@ export const createAppointment = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -122,19 +123,19 @@ export const createAppointment = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
console.error('Email failed:', err); console.error("Email failed:", err);
} }
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: 'Appointment booked successfully', message: "Appointment booked successfully",
data: appointment, data: appointment,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to create appointment', message: "Failed to create appointment",
}); });
} }
}; };
@@ -151,9 +152,11 @@ export const getAppointments = async (req, res) => {
const where = {}; const where = {};
const hasSingleDate = date && date.trim() !== ''; const hasSingleDate = date && date.trim() !== "";
const hasRange = (startDate && startDate.trim() !== '') || (endDate && endDate.trim() !== ''); const hasRange =
(startDate && startDate.trim() !== "") ||
(endDate && endDate.trim() !== "");
if (hasSingleDate) { if (hasSingleDate) {
const start = new Date(date); const start = new Date(date);
@@ -171,14 +174,14 @@ export const getAppointments = async (req, res) => {
if (!hasSingleDate && hasRange) { if (!hasSingleDate && hasRange) {
const dateFilter = {}; const dateFilter = {};
if (startDate && startDate.trim() !== '') { if (startDate && startDate.trim() !== "") {
const start = new Date(startDate); const start = new Date(startDate);
start.setHours(0, 0, 0, 0); start.setHours(0, 0, 0, 0);
dateFilter.gte = start; dateFilter.gte = start;
} }
if (endDate && endDate.trim() !== '') { if (endDate && endDate.trim() !== "") {
const end = new Date(endDate); const end = new Date(endDate);
end.setHours(23, 59, 59, 999); end.setHours(23, 59, 59, 999);
@@ -188,11 +191,11 @@ export const getAppointments = async (req, res) => {
where.date = dateFilter; where.date = dateFilter;
} }
if (search && search.trim() !== '') { if (search && search.trim() !== "") {
where.OR = [ where.OR = [
{ name: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: "insensitive" } },
{ mobileNumber: { contains: search } }, { mobileNumber: { contains: search } },
{ email: { contains: search, mode: 'insensitive' } }, { email: { contains: search, mode: "insensitive" } },
]; ];
} }
@@ -204,7 +207,7 @@ export const getAppointments = async (req, res) => {
department: true, department: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
skip, skip,
take: limit, take: limit,
@@ -230,7 +233,7 @@ export const getAppointments = async (req, res) => {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch appointments', message: "Failed to fetch appointments",
}); });
} }
}; };
@@ -254,7 +257,7 @@ export const getAppointment = async (req, res) => {
if (!appointment) { if (!appointment) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Appointment not found', message: "Appointment not found",
}); });
} }
@@ -266,7 +269,7 @@ export const getAppointment = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch appointment', message: "Failed to fetch appointment",
}); });
} }
}; };
@@ -286,7 +289,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
department: true, department: true,
}, },
orderBy: { orderBy: {
date: 'asc', date: "asc",
}, },
}); });
@@ -298,7 +301,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctor appointments', message: "Failed to fetch doctor appointments",
}); });
} }
}; };
@@ -327,7 +330,7 @@ export const getAppointmentsByDepartment = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch department appointments', message: "Failed to fetch department appointments",
}); });
} }
}; };
@@ -351,14 +354,14 @@ export const updateAppointment = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Appointment updated successfully', message: "Appointment updated successfully",
data: appointment, data: appointment,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to update appointment', message: "Failed to update appointment",
}); });
} }
}; };
@@ -377,13 +380,13 @@ export const deleteAppointment = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Appointment deleted successfully', message: "Appointment deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete appointment', message: "Failed to delete appointment",
}); });
} }
}; };
+15 -15
View File
@@ -1,24 +1,24 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { generateToken } from '../utils/jwt.js'; import {generateToken} from "../utils/jwt.js";
import { hashPassword, comparePassword } from '../utils/password.js'; import {hashPassword, comparePassword} from "../utils/password.js";
/** /**
* REGISTER * REGISTER
* POST /api/auth/register * POST /api/auth/register
*/ */
export async function register(req, res) { export async function register(req, res) {
const { username, password, role } = req.body; const {username, password, role} = req.body;
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' }); return res.status(400).json({error: "Username and password required"});
} }
const existingUser = await prisma.user.findUnique({ const existingUser = await prisma.user.findUnique({
where: { username }, where: {username},
}); });
if (existingUser) { if (existingUser) {
return res.status(409).json({ error: 'Username already exists' }); return res.status(409).json({error: "Username already exists"});
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
@@ -27,12 +27,12 @@ export async function register(req, res) {
data: { data: {
username, username,
password: hashedPassword, password: hashedPassword,
role: role || 'admin', role: role || "admin",
}, },
}); });
res.status(201).json({ res.status(201).json({
message: 'User registered successfully', message: "User registered successfully",
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
@@ -46,24 +46,24 @@ export async function register(req, res) {
* POST /api/auth/login * POST /api/auth/login
*/ */
export async function login(req, res) { export async function login(req, res) {
const { username, password } = req.body; const {username, password} = req.body;
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' }); return res.status(400).json({error: "Username and password required"});
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { username }, where: {username},
}); });
if (!user) { if (!user) {
return res.status(401).json({ error: 'Invalid credentials' }); return res.status(401).json({error: "Invalid credentials"});
} }
const isValid = await comparePassword(password, user.password); const isValid = await comparePassword(password, user.password);
if (!isValid) { if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' }); return res.status(401).json({error: "Invalid credentials"});
} }
const token = generateToken({ const token = generateToken({
@@ -72,5 +72,5 @@ export async function login(req, res) {
role: user.role, role: user.role,
}); });
res.json({ token }); res.json({token});
} }
+21 -21
View File
@@ -1,10 +1,10 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import slugify from 'slugify'; import slugify from "slugify";
/* CREATE BLOG */ /* CREATE BLOG */
export async function createBlog(req, res) { export async function createBlog(req, res) {
const { title, writer, image, content, isActive } = req.body; const {title, writer, image, content, isActive} = req.body;
try { try {
const blog = await prisma.blog.create({ const blog = await prisma.blog.create({
@@ -20,7 +20,7 @@ export async function createBlog(req, res) {
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Blog creation failed' }); res.status(500).json({error: "Blog creation failed"});
} }
} }
@@ -29,13 +29,13 @@ export async function createBlog(req, res) {
export async function getBlogs(req, res) { export async function getBlogs(req, res) {
try { try {
const blogs = await prisma.blog.findMany({ const blogs = await prisma.blog.findMany({
where: { isActive: true }, where: {isActive: true},
orderBy: { createdAt: 'desc' }, orderBy: {createdAt: "desc"},
}); });
res.json(blogs); res.json(blogs);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -44,12 +44,12 @@ export async function getBlogs(req, res) {
export async function getAllBlogs(req, res) { export async function getAllBlogs(req, res) {
try { try {
const blogs = await prisma.blog.findMany({ const blogs = await prisma.blog.findMany({
orderBy: { createdAt: 'desc' }, orderBy: {createdAt: "desc"},
}); });
res.json(blogs); res.json(blogs);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -60,16 +60,16 @@ export async function getBlog(req, res) {
const slug = req.params.slug; const slug = req.params.slug;
const blog = await prisma.blog.findUnique({ const blog = await prisma.blog.findUnique({
where: { slug }, where: {slug},
}); });
if (!blog) { if (!blog) {
return res.status(404).json({ error: 'Blog not found' }); return res.status(404).json({error: "Blog not found"});
} }
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -80,16 +80,16 @@ export async function getBlogForAdmin(req, res) {
const id = Number(req.params.id); const id = Number(req.params.id);
const blog = await prisma.blog.findUnique({ const blog = await prisma.blog.findUnique({
where: { id }, where: {id},
}); });
if (!blog) { if (!blog) {
return res.status(404).json({ error: 'Blog not found' }); return res.status(404).json({error: "Blog not found"});
} }
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -97,10 +97,10 @@ export async function getBlogForAdmin(req, res) {
export async function updateBlog(req, res) { export async function updateBlog(req, res) {
try { try {
const { title, writer, image, content } = req.body; const {title, writer, image, content} = req.body;
const blog = await prisma.blog.update({ const blog = await prisma.blog.update({
where: { id: Number(req.params.id) }, where: {id: Number(req.params.id)},
data: { data: {
title, title,
writer, writer,
@@ -111,7 +111,7 @@ export async function updateBlog(req, res) {
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -122,11 +122,11 @@ export async function deleteBlog(req, res) {
const id = Number(req.params.id); const id = Number(req.params.id);
await prisma.blog.delete({ await prisma.blog.delete({
where: { id }, where: {id},
}); });
res.json({ message: 'Blog deleted successfully' }); res.json({message: "Blog deleted successfully"});
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
+25 -24
View File
@@ -1,18 +1,19 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js'; import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from '../utils/getEmailByTypes.js'; import { getEmailsByType } from "../utils/getEmailByTypes.js";
// CREATE CANDIDATE // CREATE CANDIDATE
export const createCandidate = async (req, res) => { export const createCandidate = async (req, res) => {
try { try {
const { fullName, mobile, email, subject, coverLetter, careerId } = req.body; const { fullName, mobile, email, subject, coverLetter, careerId } =
req.body;
if (!fullName || !mobile || !email || !careerId) { if (!fullName || !mobile || !email || !careerId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Required fields missing', message: "Required fields missing",
}); });
} }
@@ -31,12 +32,12 @@ export const createCandidate = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType('CANDIDATE'); const emailList = await getEmailsByType("CANDIDATE");
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: 'New Job Application Received', subject: "New Job Application Received",
html: ` html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -75,15 +76,15 @@ export const createCandidate = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
<tr> <tr>
<td style="padding: 8px 0;"><b>Applied For:</b></td> <td style="padding: 8px 0;"><b>Applied For:</b></td>
<td style="padding: 8px 0;">${candidate.career?.post || '-'}</td> <td style="padding: 8px 0;">${candidate.career?.post || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Designation:</b></td> <td style="padding: 8px 0;"><b>Designation:</b></td>
<td style="padding: 8px 0;">${candidate.career?.designation || '-'}</td> <td style="padding: 8px 0;">${candidate.career?.designation || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Subject:</b></td> <td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td> <td style="padding: 8px 0;">${subject || "-"}</td>
</tr> </tr>
</table> </table>
@@ -99,7 +100,7 @@ export const createCandidate = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${coverLetter ? coverLetter.replace(/\n/g, '<br/>') : '-'} ${coverLetter ? coverLetter.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -117,19 +118,19 @@ export const createCandidate = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
console.error('Candidate email failed:', err); console.error("Candidate email failed:", err);
} }
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: 'Application submitted successfully', message: "Application submitted successfully",
data: candidate, data: candidate,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to create candidate', message: "Failed to create candidate",
}); });
} }
}; };
@@ -143,7 +144,7 @@ export const getCandidates = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -155,7 +156,7 @@ export const getCandidates = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch candidates', message: "Failed to fetch candidates",
}); });
} }
}; };
@@ -178,7 +179,7 @@ export const getCandidate = async (req, res) => {
if (!candidate) { if (!candidate) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Candidate not found', message: "Candidate not found",
}); });
} }
@@ -190,7 +191,7 @@ export const getCandidate = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch candidate', message: "Failed to fetch candidate",
}); });
} }
}; };
@@ -209,7 +210,7 @@ export const getCandidatesByCareer = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -221,7 +222,7 @@ export const getCandidatesByCareer = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch candidates', message: "Failed to fetch candidates",
}); });
} }
}; };
@@ -241,14 +242,14 @@ export const updateCandidate = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Candidate updated successfully', message: "Candidate updated successfully",
data: candidate, data: candidate,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to update candidate', message: "Failed to update candidate",
}); });
} }
}; };
@@ -267,13 +268,13 @@ export const deleteCandidate = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Candidate deleted successfully', message: "Candidate deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete candidate', message: "Failed to delete candidate",
}); });
} }
}; };
+22 -12
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// GET ALL CAREERS // GET ALL CAREERS
@@ -7,8 +7,8 @@ export const getAllCareers = async (req, res) => {
const { admin } = req.query; const { admin } = req.query;
const careers = await prisma.career.findMany({ const careers = await prisma.career.findMany({
where: admin === 'true' ? {} : { isActive: true }, where: admin === "true" ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
}); });
const response = careers.map((c) => ({ const response = careers.map((c) => ({
@@ -32,7 +32,7 @@ export const getAllCareers = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to fetch careers', message: "Failed to fetch careers",
}); });
} }
}; };
@@ -41,12 +41,22 @@ export const getAllCareers = async (req, res) => {
export const createCareer = async (req, res) => { export const createCareer = async (req, res) => {
try { try {
const { post, designation, qualification, experienceNeed, email, number, status, isActive, sortOrder } = req.body; const {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
isActive,
sortOrder,
} = req.body;
if (!post || !designation) { if (!post || !designation) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Post and designation are required', message: "Post and designation are required",
}); });
} }
@@ -66,14 +76,14 @@ export const createCareer = async (req, res) => {
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: 'Career created successfully', message: "Career created successfully",
data: career, data: career,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to create career', message: "Failed to create career",
}); });
} }
}; };
@@ -96,14 +106,14 @@ export const updateCareer = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'Career updated successfully', message: "Career updated successfully",
data: career, data: career,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to update career', message: "Failed to update career",
}); });
} }
}; };
@@ -120,13 +130,13 @@ export const deleteCareer = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'Career deleted successfully', message: "Career deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to delete career', message: "Failed to delete career",
}); });
} }
}; };
@@ -1,23 +1,23 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
export const getAllDepartments = async (req, res) => { export const getAllDepartments = async (req, res) => {
try { try {
const { admin } = req.query; const {admin} = req.query;
const departments = await prisma.department.findMany({ const departments = await prisma.department.findMany({
where: admin === 'true' ? {} : { isActive: true }, where: admin === "true" ? {} : {isActive: true},
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }], orderBy: [{sortOrder: "asc"}, {name: "asc"}],
}); });
const response = departments.map((dep) => ({ const response = departments.map((dep) => ({
departmentId: dep.departmentId, departmentId: dep.departmentId,
name: dep.name, name: dep.name,
image: dep.image ?? '', image: dep.image ?? "",
para1: dep.para1 ?? '', para1: dep.para1 ?? "",
para2: dep.para2 ?? '', para2: dep.para2 ?? "",
para3: dep.para3 ?? '', para3: dep.para3 ?? "",
facilities: dep.facilities ?? '', facilities: dep.facilities ?? "",
services: dep.services ?? '', services: dep.services ?? "",
isActive: dep.isActive, isActive: dep.isActive,
sortOrder: dep.sortOrder, sortOrder: dep.sortOrder,
})); }));
@@ -30,19 +30,19 @@ export const getAllDepartments = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to fetch departments', message: "Failed to fetch departments",
}); });
} }
}; };
export const getDepartmentByName = async (req, res) => { export const getDepartmentByName = async (req, res) => {
try { try {
const { name } = req.query; const {name} = req.query;
if (!name) { if (!name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Department name is required', message: "Department name is required",
}); });
} }
@@ -56,19 +56,19 @@ export const getDepartmentByName = async (req, res) => {
if (!department) { if (!department) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Department not found or inactive', message: "Department not found or inactive",
}); });
} }
const response = { const response = {
departmentId: department.departmentId, departmentId: department.departmentId,
name: department.name, name: department.name,
image: department.image ?? '', image: department.image ?? "",
para1: department.para1 ?? '', para1: department.para1 ?? "",
para2: department.para2 ?? '', para2: department.para2 ?? "",
para3: department.para3 ?? '', para3: department.para3 ?? "",
facilities: department.facilities ?? '', facilities: department.facilities ?? "",
services: department.services ?? '', services: department.services ?? "",
isActive: department.isActive, isActive: department.isActive,
sortOrder: department.sortOrder, sortOrder: department.sortOrder,
}; };
@@ -81,17 +81,30 @@ export const getDepartmentByName = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to fetch department', message: "Failed to fetch department",
}); });
} }
}; };
export async function createDepartment(req, res) { export async function createDepartment(req, res) {
try { try {
const { departmentId, name, image, para1, para2, para3, facilities, services, isActive, sortOrder } = req.body; const {
departmentId,
name,
image,
para1,
para2,
para3,
facilities,
services,
isActive,
sortOrder,
} = req.body;
if (!departmentId || !name) { if (!departmentId || !name) {
return res.status(400).json({ error: 'departmentId and name are required' }); return res
.status(400)
.json({error: "departmentId and name are required"});
} }
const department = await prisma.department.create({ const department = await prisma.department.create({
@@ -110,63 +123,63 @@ export async function createDepartment(req, res) {
}); });
res.status(201).json({ res.status(201).json({
message: 'Department created successfully', message: "Department created successfully",
data: department, data: department,
}); });
} catch (error) { } catch (error) {
if (error.code === 'P2002') { if (error.code === "P2002") {
return res.status(409).json({ error: 'Department already exists' }); return res.status(409).json({error: "Department already exists"});
} }
console.error(error); console.error(error);
res.status(500).json({ error: 'Failed to create department' }); res.status(500).json({error: "Failed to create department"});
} }
} }
export const updateDepartment = async (req, res) => { export const updateDepartment = async (req, res) => {
try { try {
const { departmentId } = req.params; const {departmentId} = req.params;
const updateData = { ...req.body }; const updateData = {...req.body};
if (updateData.sortOrder !== undefined) { if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder); updateData.sortOrder = Number(updateData.sortOrder);
} }
const department = await prisma.department.update({ const department = await prisma.department.update({
where: { departmentId }, where: {departmentId},
data: updateData, data: updateData,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'Department updated successfully', message: "Department updated successfully",
data: department, data: department,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to update department', message: "Failed to update department",
}); });
} }
}; };
export const deleteDepartment = async (req, res) => { export const deleteDepartment = async (req, res) => {
try { try {
const { departmentId } = req.params; const {departmentId} = req.params;
await prisma.department.delete({ await prisma.department.delete({
where: { departmentId }, where: {departmentId},
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'Department deleted successfully', message: "Department deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to delete department', message: "Failed to delete department",
}); });
} }
}; };
+83 -388
View File
@@ -1,59 +1,34 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// get doctors // get doctors
export const getAllDoctors = async (req, res) => { export const getAllDoctors = async (req, res) => {
try { try {
const { admin } = req.query; const {admin} = req.query;
const doctors = await prisma.doctor.findMany({ const doctors = await prisma.doctor.findMany({
where: admin === 'true' ? {} : { isActive: true }, where: admin === "true" ? {} : {isActive: true},
include: { include: {
seo: true,
departments: { departments: {
include: { include: {
department: true, department: true,
timing: true, timing: true,
}, },
}, },
specializations: {
orderBy: {
createdAt: 'asc',
},
},
}, },
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }], orderBy: [{globalSortOrder: "asc"}, {name: "asc"}],
}); });
const formatted = doctors.map((doc, index) => ({ const formatted = doctors.map((doc, index) => ({
SL_NO: String(index + 1), SL_NO: String(index + 1),
doctorId: doc.doctorId, doctorId: doc.doctorId,
name: doc.name, name: doc.name,
image: doc.image ?? '', image: doc.image ?? "",
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive, isActive: doc.isActive,
isFeatured: doc.isFeatured,
experience: doc.experience,
professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder, globalSortOrder: doc.globalSortOrder,
specializations: doc.specializations.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})),
seo: {
seoTitle: doc.seo?.seoTitle ?? '',
metaDescription: doc.seo?.metaDescription ?? '',
focusKeyphrase: doc.seo?.focusKeyphrase ?? '',
slug: doc.seo?.slug ?? '',
tags: doc.seo?.tags ?? [],
ogTitle: doc.seo?.ogTitle ?? '',
ogDescription: doc.seo?.ogDescription ?? '',
ogImage: doc.seo?.ogImage ?? '',
},
departments: doc.departments.map((d) => { departments: doc.departments.map((d) => {
const t = d.timing || {}; const t = d.timing || {};
const timingArray = [ const timingArray = [
@@ -70,7 +45,7 @@ export const getAllDoctors = async (req, res) => {
return { return {
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
timing: timingArray.join(' & '), timing: timingArray.join(" & "),
deptSortOrder: d.sortOrder, deptSortOrder: d.sortOrder,
}; };
}), }),
@@ -84,7 +59,7 @@ export const getAllDoctors = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctors', message: "Failed to fetch doctors",
}); });
} }
}; };
@@ -93,17 +68,11 @@ export const getAllDoctors = async (req, res) => {
export const getDoctorByDoctorId = async (req, res) => { export const getDoctorByDoctorId = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
const { admin } = req.query;
const doctor = await prisma.doctor.findFirst({ const doctor = await prisma.doctor.findUnique({
where: { where: {doctorId},
doctorId,
...(admin === 'true' ? {} : { isActive: true }),
},
include: { include: {
seo: true,
specializations: true,
departments: { departments: {
include: { include: {
department: true, department: true,
@@ -116,37 +85,17 @@ export const getDoctorByDoctorId = async (req, res) => {
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Doctor not found', message: "Doctor not found",
}); });
} }
const response = { const response = {
doctorId: doctor.doctorId, doctorId: doctor.doctorId,
name: doctor.name, name: doctor.name,
image: doctor.image ?? '', image: doctor.image ?? "",
designation: doctor.designation, designation: doctor.designation,
workingStatus: doctor.workingStatus, workingStatus: doctor.workingStatus,
qualification: doctor.qualification, qualification: doctor.qualification,
experience: doctor.experience,
professionalSummary: doctor.professionalSummary,
isActive: doctor.isActive,
isFeatured: doctor.isFeatured,
seo: {
seoTitle: doctor.seo?.seoTitle ?? '',
metaDescription: doctor.seo?.metaDescription ?? '',
focusKeyphrase: doctor.seo?.focusKeyphrase ?? '',
slug: doctor.seo?.slug ?? '',
tags: doctor.seo?.tags ?? [],
ogTitle: doctor.seo?.ogTitle ?? '',
ogDescription: doctor.seo?.ogDescription ?? '',
ogImage: doctor.seo?.ogImage ?? '',
},
specializations:
doctor.specializations?.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})) ?? [],
departments: doctor.departments.map((d) => ({ departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
@@ -162,7 +111,7 @@ export const getDoctorByDoctorId = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctor', message: "Failed to fetch doctor",
}); });
} }
}; };
@@ -170,52 +119,43 @@ export const getDoctorByDoctorId = async (req, res) => {
// get doctors by department // get doctors by department
export const getDoctorsByDepartmentId = async (req, res) => { export const getDoctorsByDepartmentId = async (req, res) => {
try { try {
const { Department_ID } = req.query; const {Department_ID} = req.query;
if (!Department_ID) { if (!Department_ID) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Department_ID is required', message: "Department_ID is required",
}); });
} }
const department = await prisma.department.findUnique({ const department = await prisma.department.findUnique({
where: { departmentId: Department_ID }, where: {departmentId: Department_ID},
}); });
if (!department) { if (!department) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Department not found', message: "Department not found",
}); });
} }
const doctorsInDept = await prisma.doctorDepartment.findMany({ const doctorsInDept = await prisma.doctorDepartment.findMany({
where: { where: {
departmentId: department.id, departmentId: department.id,
doctor: { isActive: true }, doctor: {isActive: true},
}, },
include: { include: {
doctor: { doctor: true,
include: {
seo: {
select: {
slug: true,
},
},
},
},
}, },
orderBy: { sortOrder: 'asc' }, orderBy: {sortOrder: "asc"},
}); });
const result = doctorsInDept.map((d) => ({ const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId, GG_ID: d.doctor.doctorId,
Name: d.doctor.name, Name: d.doctor.name,
image: d.doctor.image ?? '', image: d.doctor.image ?? "",
designation: d.doctor.designation, designation: d.doctor.designation,
hierarchyOrder: d.sortOrder, hierarchyOrder: d.sortOrder,
slug: d.doctor.seo?.slug ?? '',
})); }));
res.status(200).json({ res.status(200).json({
@@ -226,7 +166,7 @@ export const getDoctorsByDepartmentId = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctors', message: "Failed to fetch doctors",
}); });
} }
}; };
@@ -242,51 +182,9 @@ export const createDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
isFeatured,
globalSortOrder, globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
ogTitle,
ogDescription,
ogImage,
} = req.body; } = req.body;
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
// Open Graph
ogTitle,
ogDescription,
ogImage,
},
});
const doctor = await prisma.doctor.create({ const doctor = await prisma.doctor.create({
data: { data: {
@@ -296,18 +194,15 @@ export const createDoctor = async (req, res) => {
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
isFeatured: isFeatured !== undefined ? isFeatured : false, globalSortOrder:
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0, globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
}, },
}); });
for (const dep of departments) { for (const dep of departments) {
const department = await prisma.department.findUnique({ const department = await prisma.department.findUnique({
where: { departmentId: dep.departmentId }, where: {departmentId: dep.departmentId},
}); });
if (!department) continue; if (!department) continue;
@@ -329,27 +224,16 @@ export const createDoctor = async (req, res) => {
}); });
} }
} }
if (specializations?.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: 'Doctor created successfully', message: "Doctor created successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to create doctor', message: "Failed to create doctor",
}); });
} }
}; };
@@ -357,7 +241,7 @@ export const createDoctor = async (req, res) => {
//update doctors //update doctors
export const updateDoctor = async (req, res) => { export const updateDoctor = async (req, res) => {
try { try {
const { doctorId, action } = req.params; const {doctorId} = req.params;
const { const {
name, name,
designation, designation,
@@ -365,261 +249,125 @@ export const updateDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
isFeatured,
globalSortOrder, globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
ogTitle,
ogDescription,
focusKeyphrase,
slug,
tags,
ogImage,
specializations,
} = req.body; } = req.body;
if (!doctorId) { const doctor = await prisma.doctor.findUnique({where: {doctorId}});
return res.status(400).json({ if (!doctor)
success: false, return res
message: 'Doctor ID is required', .status(404)
}); .json({success: false, message: "Doctor not found"});
}
const doctor = await prisma.doctor.findUnique({ where: { doctorId } });
if (!doctor) return res.status(404).json({ success: false, message: 'Doctor not found' });
if (action === 'toggleStatus') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isActive: !doctor.isActive,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`,
});
}
if (action === 'toggleFeatured') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isFeatured: !doctor.isFeatured,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isFeatured ? 'removed from featured' : 'marked as featured'} successfully`,
});
}
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
await prisma.doctor.update({ await prisma.doctor.update({
where: { id: doctor.id }, where: {id: doctor.id},
data: { data: {
name, name,
designation, designation,
image, image,
workingStatus, workingStatus,
qualification, qualification,
isActive: isActive !== undefined ? isActive : undefined, isActive,
isFeatured: isFeatured !== undefined ? isFeatured : undefined, globalSortOrder:
experience: experience ? Number(experience) : null, globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
professionalSummary,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
}, },
}); });
if (doctor.seoId) { const hasTimingData = departments?.some(
await prisma.seo.update({ (dep) => dep.timing && Object.keys(dep.timing).length > 0,
where: { );
id: doctor.seoId,
},
data: {
seoTitle,
metaDescription,
ogTitle,
ogDescription,
ogImage,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
} else {
const seo = await prisma.seo.create({
data: {
ogImage,
metaDescription,
seoTitle,
ogDescription,
ogTitle,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
await prisma.doctor.update({ if (departments && Array.isArray(departments) && hasTimingData) {
where: {
id: doctor.id,
},
data: {
seoId: seo.id,
},
});
}
// Update Departments & Timings
if (Array.isArray(departments)) {
const oldRelations = await prisma.doctorDepartment.findMany({ const oldRelations = await prisma.doctorDepartment.findMany({
where: { where: {doctorId: doctor.id},
doctorId: doctor.id,
},
include: {
timing: true,
},
}); });
// Delete old timings
for (const rel of oldRelations) { for (const rel of oldRelations) {
if (rel.timing) { await prisma.doctorTiming.deleteMany({
await prisma.doctorTiming.deleteMany({ where: {doctorDepartmentId: rel.id},
where: { });
doctorDepartmentId: rel.id,
},
});
}
} }
// Delete old departments
await prisma.doctorDepartment.deleteMany({ await prisma.doctorDepartment.deleteMany({
where: { where: {doctorId: doctor.id},
doctorId: doctor.id,
},
}); });
// Recreate departments + timings
for (const dep of departments) { for (const dep of departments) {
const department = await prisma.department.findUnique({ const targetDept = await prisma.department.findUnique({
where: { where: {departmentId: dep.departmentId},
departmentId: dep.departmentId,
},
}); });
if (!targetDept) continue;
if (!department) continue; const newDD = await prisma.doctorDepartment.create({
const doctorDepartment = await prisma.doctorDepartment.create({
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: targetDept.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0, sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
if (dep.timing && Object.keys(dep.timing).length > 0) { if (dep.timing) {
const { id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming } = dep.timing; const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing;
await prisma.doctorTiming.create({ await prisma.doctorTiming.create({
data: { data: {doctorDepartmentId: newDD.id, ...cleanTiming},
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
}); });
} }
} }
} }
// Update Specializations res
if (Array.isArray(specializations)) { .status(200)
await prisma.doctorSpecialization.deleteMany({ .json({success: true, message: "Doctor updated successfully"});
where: {
doctorId: doctor.id,
},
});
if (specializations.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
}
res.status(200).json({ success: true, message: 'Doctor updated successfully' });
} catch (error) { } catch (error) {
console.error('Update Error:', error); console.error("Update Error:", error);
res.status(500).json({ success: false, message: 'Failed to update doctor' }); res.status(500).json({success: false, message: "Failed to update doctor"});
} }
}; };
//delete doctor //delete doctor
export const deleteDoctor = async (req, res) => { export const deleteDoctor = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: { doctorId }, where: {doctorId},
}); });
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Doctor not found', message: "Doctor not found",
}); });
} }
const doctorDepartments = await prisma.doctorDepartment.findMany({ const doctorDepartments = await prisma.doctorDepartment.findMany({
where: { doctorId: doctor.id }, where: {doctorId: doctor.id},
}); });
for (const dd of doctorDepartments) { for (const dd of doctorDepartments) {
await prisma.doctorTiming.deleteMany({ await prisma.doctorTiming.deleteMany({
where: { doctorDepartmentId: dd.id }, where: {doctorDepartmentId: dd.id},
}); });
} }
await prisma.doctorDepartment.deleteMany({ await prisma.doctorDepartment.deleteMany({
where: { doctorId: doctor.id }, where: {doctorId: doctor.id},
}); });
await prisma.doctor.delete({ await prisma.doctor.delete({
where: { id: doctor.id }, where: {id: doctor.id},
}); });
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Doctor deleted successfully', message: "Doctor deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete doctor', message: "Failed to delete doctor",
}); });
} }
}; };
@@ -644,14 +392,14 @@ export const getDoctorTimings = async (req, res) => {
return { return {
Doctor_ID: doc.doctorId, Doctor_ID: doc.doctorId,
Doctor: doc.name, Doctor: doc.name,
Monday: timing.monday || '', Monday: timing.monday || "",
Tuesday: timing.tuesday || '', Tuesday: timing.tuesday || "",
Wednesday: timing.wednesday || '', Wednesday: timing.wednesday || "",
Thursday: timing.thursday || '', Thursday: timing.thursday || "",
Friday: timing.friday || '', Friday: timing.friday || "",
Saturday: timing.saturday || '', Saturday: timing.saturday || "",
Sunday: timing.sunday || '', Sunday: timing.sunday || "",
Additional: timing.additional || '', Additional: timing.additional || "",
}; };
}); });
@@ -663,7 +411,7 @@ export const getDoctorTimings = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctor timings', message: "Failed to fetch doctor timings",
}); });
} }
}; };
@@ -671,14 +419,10 @@ export const getDoctorTimings = async (req, res) => {
export const getDoctorTimingById = async (req, res) => { export const getDoctorTimingById = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
const { admin } = req.query;
const doctor = await prisma.doctor.findFirst({ const doctor = await prisma.doctor.findUnique({
where: { where: {doctorId},
doctorId,
...(admin === 'true' ? {} : { isActive: true }),
},
include: { include: {
departments: { departments: {
include: { include: {
@@ -692,7 +436,7 @@ export const getDoctorTimingById = async (req, res) => {
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Doctor not found', message: "Doctor not found",
}); });
} }
@@ -715,56 +459,7 @@ export const getDoctorTimingById = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch doctor timing', message: "Failed to fetch doctor timing",
});
}
};
export const getFeaturedDoctors = async (req, res) => {
try {
const doctors = await prisma.doctor.findMany({
where: {
isActive: true,
isFeatured: true,
},
include: {
seo: {
select: {
slug: true,
},
},
departments: {
include: {
department: true,
},
},
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
});
const data = doctors.map((doc) => ({
doctorId: doc.doctorId,
name: doc.name,
image: doc.image ?? '',
designation: doc.designation,
qualification: doc.qualification,
experience: doc.experience,
slug: doc.seo?.slug ?? '',
departments: doc.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
})),
}));
res.status(200).json({
success: true,
data,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch featured doctors',
}); });
} }
}; };
@@ -1,14 +1,14 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// CREATE // CREATE
export const createEmailConfig = async (req, res) => { export const createEmailConfig = async (req, res) => {
try { try {
const { name, email, type, isActive } = req.body; const {name, email, type, isActive} = req.body;
if (!name || !email || !type) { if (!name || !email || !type) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Name, Email and Type are required', message: "Name, Email and Type are required",
}); });
} }
@@ -23,14 +23,14 @@ export const createEmailConfig = async (req, res) => {
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: 'Email config created', message: "Email config created",
data: newEmail, data: newEmail,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to create email config', message: "Failed to create email config",
}); });
} }
}; };
@@ -40,7 +40,7 @@ export const getEmailConfigs = async (req, res) => {
try { try {
const emails = await prisma.emailConfig.findMany({ const emails = await prisma.emailConfig.findMany({
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -52,7 +52,7 @@ export const getEmailConfigs = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch email configs', message: "Failed to fetch email configs",
}); });
} }
}; };
@@ -60,7 +60,7 @@ export const getEmailConfigs = async (req, res) => {
// GET SINGLE // GET SINGLE
export const getEmailConfig = async (req, res) => { export const getEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const email = await prisma.emailConfig.findUnique({ const email = await prisma.emailConfig.findUnique({
where: { where: {
@@ -71,7 +71,7 @@ export const getEmailConfig = async (req, res) => {
if (!email) { if (!email) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Email config not found', message: "Email config not found",
}); });
} }
@@ -83,7 +83,7 @@ export const getEmailConfig = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch email config', message: "Failed to fetch email config",
}); });
} }
}; };
@@ -91,7 +91,7 @@ export const getEmailConfig = async (req, res) => {
// UPDATE // UPDATE
export const updateEmailConfig = async (req, res) => { export const updateEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const updated = await prisma.emailConfig.update({ const updated = await prisma.emailConfig.update({
where: { where: {
@@ -102,14 +102,14 @@ export const updateEmailConfig = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Email config updated', message: "Email config updated",
data: updated, data: updated,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to update email config', message: "Failed to update email config",
}); });
} }
}; };
@@ -117,7 +117,7 @@ export const updateEmailConfig = async (req, res) => {
// DELETE // DELETE
export const deleteEmailConfig = async (req, res) => { export const deleteEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
await prisma.emailConfig.delete({ await prisma.emailConfig.delete({
where: { where: {
@@ -127,13 +127,13 @@ export const deleteEmailConfig = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Email config deleted', message: "Email config deleted",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete email config', message: "Failed to delete email config",
}); });
} }
}; };
@@ -1,518 +0,0 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const getAllCategories = async (req, res) => {
try {
const { admin } = req.query;
const categories = await prisma.healthCheckCategory.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: { sortOrder: 'asc' },
include: {
_count: { select: { packages: true } },
},
});
return res.status(200).json({ success: true, data: categories });
} catch (error) {
return res.status(500).json({ success: false, message: 'Failed to fetch categories' });
}
};
export const createCategory = async (req, res) => {
try {
const { name, slug, description, isActive, sortOrder } = req.body;
const category = await prisma.healthCheckCategory.create({
data: {
name,
slug: slug || null,
description,
isActive: isActive ?? true,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
},
});
return res.status(201).json({ success: true, message: 'Category created', data: category });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create category' });
}
};
export const updateCategory = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data._count;
delete data.createdAt;
delete data.updatedAt;
if (data.sortOrder !== undefined) data.sortOrder = Number(data.sortOrder);
if (data.slug === '') data.slug = null;
const updatedCategory = await prisma.$transaction(async (tx) => {
const category = await tx.healthCheckCategory.update({
where: { id: Number(id) },
data,
});
if (data.isActive === false) {
await tx.healthPackage.updateMany({
where: { categoryId: Number(id) },
data: { isActive: false },
});
}
return category;
});
return res.status(200).json({
success: true,
message: 'Category updated',
data: updatedCategory,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to update category' });
}
};
export const deleteCategory = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthCheckCategory.delete({
where: { id: Number(id) },
});
return res.status(200).json({ success: true, message: 'Category deleted successfully' });
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete category. Ensure no packages are linked to it.',
});
}
};
export const getAllPackages = async (req, res) => {
try {
const { admin, categorySlug } = req.query;
const packages = await prisma.healthPackage.findMany({
where: {
AND: [admin === 'true' ? {} : { isActive: true }, categorySlug ? { category: { slug: categorySlug } } : {}],
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({ success: true, data: packages });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch packages' });
}
};
export const createPackage = async (req, res) => {
try {
const {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId,
isActive,
isFeatured,
sortOrder,
seo,
} = req.body;
const healthPackage = await prisma.healthPackage.create({
data: {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId: Number(categoryId),
isActive: isActive ?? true,
isFeatured: isFeatured ?? false,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
...(seo && {
seo: {
create: {
seoTitle: seo.seoTitle,
metaDescription: seo.metaDescription,
focusKeyphrase: seo.focusKeyphrase,
slug: slug,
tags: seo.tags || [],
ogTitle: seo.ogTitle,
ogDescription: seo.ogDescription,
ogImage: seo.ogImage,
},
},
}),
},
include: {
category: true,
seo: true,
},
});
return res.status(201).json({ success: true, message: 'Package created', data: healthPackage });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create package' });
}
};
export const updatePackage = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data.category;
delete data.createdAt;
delete data.updatedAt;
delete data.seoId;
if (data.categoryId) data.categoryId = Number(data.categoryId);
if (data.sortOrder) data.sortOrder = Number(data.sortOrder);
const existingPackage = await prisma.healthPackage.findUnique({
where: { id: Number(id) },
select: { slug: true },
});
const seoSlug = data.slug || existingPackage.slug;
const updated = await prisma.healthPackage.update({
where: { id: Number(id) },
data: {
...data,
seo: data.seo
? {
upsert: {
create: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
update: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
},
}
: undefined,
},
include: {
category: true,
seo: true,
},
});
return res.status(200).json({ success: true, message: 'Package updated', data: updated });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Update failed' });
}
};
export const deletePackage = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthPackage.delete({
where: { id: Number(id) },
});
return res.status(200).json({
success: true,
message: 'Package deleted',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Delete failed',
});
}
};
export const createPackageInquiry = async (req, res) => {
try {
const { fullName, mobileNumber, email, age, gender, preferredDate, packageId, message } = req.body;
const inquiry = await prisma.healthPackageInquiry.create({
data: {
fullName,
mobileNumber,
email,
age: age ? Number(age) : null,
gender,
preferredDate: preferredDate ? new Date(preferredDate) : null,
message,
packageId: Number(packageId),
},
include: {
healthPackage: true,
},
});
try {
const emailList = await getEmailsByType('HCINQUIRY');
if (emailList) {
await sendEmail({
to: emailList,
subject: 'New Health Checkup Package Inquiry',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Health Checkup Package Inquiry
</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Inquirer Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobileNumber}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Age:</b></td>
<td style="padding: 8px 0;">${age || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Gender:</b></td>
<td style="padding: 8px 0;">${gender || '-'}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Package Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Package Name:</b></td>
<td style="padding: 8px 0;">${inquiry.healthPackage?.name || 'Unknown Package'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Preferred Date:</b></td>
<td style="padding: 8px 0;">
${
preferredDate
? new Date(preferredDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: 'Not specified'
}
</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This inquiry was submitted via the GG Hospital website.
</div>
</div>
</div>
`,
});
}
} catch (err) {
console.error('Email failed:', err);
}
return res.status(201).json({
success: true,
message: 'Booking inquiry sent successfully',
data: inquiry,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to submit inquiry' });
}
};
export const getPackageBySlug = async (req, res) => {
try {
const { slug } = req.params;
const healthPackage = await prisma.healthPackage.findFirst({
where: { slug, isActive: true },
include: {
category: true,
seo: true,
},
});
if (!healthPackage) {
return res.status(404).json({ success: false, message: 'Package not found' });
}
return res.status(200).json({
success: true,
data: healthPackage,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch package' });
}
};
export const getAllInquiries = async (req, res) => {
try {
const { page = 1, limit = 10, filterDate, startDate, endDate } = req.query;
const queryPage = parseInt(page);
const queryLimit = parseInt(limit);
const skip = (queryPage - 1) * queryLimit;
let where = {};
if (filterDate) {
where.preferredDate = {
gte: new Date(`${filterDate}T00:00:00.000Z`),
lte: new Date(`${filterDate}T23:59:59.999Z`),
};
} else if (startDate || endDate) {
where.preferredDate = {};
if (startDate) {
where.preferredDate.gte = new Date(`${startDate}T00:00:00.000Z`);
}
if (endDate) {
where.preferredDate.lte = new Date(`${endDate}T23:59:59.999Z`);
}
}
const [total, inquiries] = await prisma.$transaction([
prisma.healthPackageInquiry.count({ where }),
prisma.healthPackageInquiry.findMany({
where,
skip,
take: queryLimit,
include: {
healthPackage: {
include: {
category: true,
},
},
},
orderBy: { createdAt: 'desc' },
}),
]);
return res.status(200).json({
success: true,
data: inquiries,
pagination: {
total,
page: queryPage,
limit: queryLimit,
totalPages: Math.ceil(total / queryLimit),
},
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' });
}
};
export const getFeaturedPackages = async (req, res) => {
try {
const packages = await prisma.healthPackage.findMany({
where: {
isActive: true,
isFeatured: true,
category: {
isActive: true,
},
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({
success: true,
data: packages,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch featured packages',
});
}
};
@@ -1,203 +0,0 @@
import prisma from '../prisma/client.js';
export const createHomepageBanner = async (req, res) => {
try {
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
if (!mediaType || !desktopMediaUrl) {
return res.status(400).json({
success: false,
message: 'Media type and desktop media URL are required',
});
}
const banner = await prisma.homepageBanner.create({
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.status(201).json({
success: true,
data: banner,
message: 'Homepage banner created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create homepage banner',
});
}
};
export const getHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banners',
});
}
};
export const getActiveHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch active homepage banners',
});
}
};
export const getHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const banner = await prisma.homepageBanner.findUnique({
where: {
id: Number(id),
},
});
if (!banner) {
return res.status(404).json({
success: false,
message: 'Homepage banner not found',
});
}
res.json({
success: true,
data: banner,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banner',
});
}
};
export const updateHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
const banner = await prisma.homepageBanner.update({
where: {
id: Number(id),
},
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.json({
success: true,
data: banner,
message: 'Homepage banner updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update homepage banner',
});
}
};
export const deleteHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
await prisma.homepageBanner.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Homepage banner deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete homepage banner',
});
}
};
+35 -23
View File
@@ -1,12 +1,22 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export const bulkImportExcelData = async (req, res) => { export const bulkImportExcelData = async (req, res) => {
try { try {
const { departments, doctors, timings, careers, inquiries, academics, appointments, candidates, news } = req.body; const {
departments,
doctors,
timings,
careers,
inquiries,
academics,
appointments,
candidates,
news,
} = req.body;
console.log('🚀 Starting Robust Data Import...'); console.log("🚀 Starting Robust Data Import...");
// 1. DEPARTMENTS // 1. DEPARTMENTS
if (departments) { if (departments) {
@@ -44,14 +54,14 @@ export const bulkImportExcelData = async (req, res) => {
update: { update: {
name: row.Name?.toString(), name: row.Name?.toString(),
designation: row.Designation?.toString() || null, designation: row.Designation?.toString() || null,
workingStatus: row['Working Status']?.toString() || null, workingStatus: row["Working Status"]?.toString() || null,
qualification: row.Qualification?.toString() || null, qualification: row.Qualification?.toString() || null,
}, },
create: { create: {
doctorId: row.GG_ID.toString(), doctorId: row.GG_ID.toString(),
name: row.Name?.toString(), name: row.Name?.toString(),
designation: row.Designation?.toString() || null, designation: row.Designation?.toString() || null,
workingStatus: row['Working Status']?.toString() || null, workingStatus: row["Working Status"]?.toString() || null,
qualification: row.Qualification?.toString() || null, qualification: row.Qualification?.toString() || null,
}, },
}); });
@@ -90,8 +100,8 @@ export const bulkImportExcelData = async (req, res) => {
if (doctor && doctor.departments.length > 0) { if (doctor && doctor.departments.length > 0) {
const doctorDeptId = doctor.departments[0].id; const doctorDeptId = doctor.departments[0].id;
const rawAdd = row.Additional?.toString() || ''; const rawAdd = row.Additional?.toString() || "";
const rawMon = row.Monday?.toString() || ''; const rawMon = row.Monday?.toString() || "";
const isAppt = (val) => /appointment|basis|on call/i.test(val); const isAppt = (val) => /appointment|basis|on call/i.test(val);
let finalAdd = rawAdd; let finalAdd = rawAdd;
@@ -137,7 +147,7 @@ export const bulkImportExcelData = async (req, res) => {
experienceNeed: row.ExperienceNeed?.toString() || null, experienceNeed: row.ExperienceNeed?.toString() || null,
email: row.HiringEmail?.toString() || null, email: row.HiringEmail?.toString() || null,
number: row.Number?.toString() || null, number: row.Number?.toString() || null,
status: row.Status?.toString() || 'new', status: row.Status?.toString() || "new",
}; };
if (cId) { if (cId) {
await prisma.career.upsert({ await prisma.career.upsert({
@@ -158,7 +168,7 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.inquiry.create({ await prisma.inquiry.create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
number: row.Number?.toString() || '', number: row.Number?.toString() || "",
emailId: row.EmailId?.toString() || null, emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null, subject: row.Subject?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
@@ -175,10 +185,10 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.academicsResearch.create({ await prisma.academicsResearch.create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
number: row.Number?.toString() || '', number: row.Number?.toString() || "",
emailId: row.EmailId?.toString() || null, emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null, // Force String subject: row.Subject?.toString() || null, // Force String
courseName: row['Course Name']?.toString() || null, courseName: row["Course Name"]?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
createdAt: row.Date ? new Date(row.Date) : new Date(), createdAt: row.Date ? new Date(row.Date) : new Date(),
}, },
@@ -191,7 +201,7 @@ export const bulkImportExcelData = async (req, res) => {
for (const row of appointments) { for (const row of appointments) {
if (!row.FullName) continue; if (!row.FullName) continue;
const doctorName = row.Doctor?.toString(); const doctorName = row.Doctor?.toString();
const departmentName = row['Department Id']?.toString(); const departmentName = row["Department Id"]?.toString();
const doctor = await prisma.doctor.findFirst({ const doctor = await prisma.doctor.findFirst({
where: { name: doctorName }, where: { name: doctorName },
@@ -204,11 +214,11 @@ export const bulkImportExcelData = async (req, res) => {
if (!value) return new Date(); if (!value) return new Date();
// Excel numeric date // Excel numeric date
if (typeof value === 'number') { if (typeof value === "number") {
return new Date((value - 25569) * 86400 * 1000); return new Date((value - 25569) * 86400 * 1000);
} }
if (typeof value === 'string') { if (typeof value === "string") {
const v = value.trim(); const v = value.trim();
// Handle DD/MM/YYYY // Handle DD/MM/YYYY
@@ -228,15 +238,15 @@ export const bulkImportExcelData = async (req, res) => {
} }
} }
console.warn('⚠️ Invalid date, using current date:', value); console.warn("⚠️ Invalid date, using current date:", value);
return new Date(); return new Date();
}; };
if (doctor && department) { if (doctor && department) {
await prisma.appointment.create({ await prisma.appointment.create({
data: { data: {
name: row.FullName.toString(), name: row.FullName.toString(),
mobileNumber: row.Number?.toString() || '', mobileNumber: row.Number?.toString() || "",
email: row['Email Id']?.toString() || null, email: row["Email Id"]?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
date: parseDate(row.Date), date: parseDate(row.Date),
doctorId: doctor.doctorId, doctorId: doctor.doctorId,
@@ -255,10 +265,10 @@ export const bulkImportExcelData = async (req, res) => {
.create({ .create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
mobile: row.Number?.toString() || '', mobile: row.Number?.toString() || "",
email: row.EmailId?.toString() || '', email: row.EmailId?.toString() || "",
subject: row.Subject?.toString() || '', subject: row.Subject?.toString() || "",
coverLetter: row['Cover Letter']?.toString() || '', coverLetter: row["Cover Letter"]?.toString() || "",
careerId: parseInt(row.CareerId), careerId: parseInt(row.CareerId),
createdAt: row.Date ? new Date(row.Date) : new Date(), createdAt: row.Date ? new Date(row.Date) : new Date(),
}, },
@@ -284,9 +294,11 @@ export const bulkImportExcelData = async (req, res) => {
} }
} }
res.status(200).json({ success: true, message: '✅ Import completed successfully!' }); res
.status(200)
.json({ success: true, message: "✅ Import completed successfully!" });
} catch (error) { } catch (error) {
console.error('❌ Bulk Import Error:', error); console.error("❌ Bulk Import Error:", error);
res.status(500).json({ success: false, error: error.message }); res.status(500).json({ success: false, error: error.message });
} }
}; };
+21 -21
View File
@@ -1,17 +1,17 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js'; import {sendEmail} from "../utils/sendEmail.js";
import { getEmailsByType } from '../utils/getEmailByTypes.js'; import {getEmailsByType} from "../utils/getEmailByTypes.js";
/* CREATE INQUIRY */ /* CREATE INQUIRY */
export const createInquiry = async (req, res) => { export const createInquiry = async (req, res) => {
try { try {
const { fullName, number, emailId, subject, message } = req.body; const {fullName, number, emailId, subject, message} = req.body;
if (!fullName || !number) { if (!fullName || !number) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Full name and number are required', message: "Full name and number are required",
}); });
} }
@@ -25,12 +25,12 @@ export const createInquiry = async (req, res) => {
}, },
}); });
try { try {
const emailList = await getEmailsByType('INQUIRY'); const emailList = await getEmailsByType("INQUIRY");
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: 'New Inquiry Received', subject: "New Inquiry Received",
html: ` html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -78,7 +78,7 @@ export const createInquiry = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -96,20 +96,20 @@ export const createInquiry = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
console.error('Inquiry email failed:', err); console.error("Inquiry email failed:", err);
} }
res.status(200).json({ res.status(200).json({
success: true, success: true,
status: 200, status: 200,
data: inquiry, data: inquiry,
message: 'Inquiry added successfully', message: "Inquiry added successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to add inquiry', message: "Failed to add inquiry",
}); });
} }
}; };
@@ -119,7 +119,7 @@ export const getInquiries = async (req, res) => {
try { try {
const inquiries = await prisma.inquiry.findMany({ const inquiries = await prisma.inquiry.findMany({
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -130,7 +130,7 @@ export const getInquiries = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch inquiries', message: "Failed to fetch inquiries",
}); });
} }
}; };
@@ -138,16 +138,16 @@ export const getInquiries = async (req, res) => {
/* GET SINGLE */ /* GET SINGLE */
export const getInquiry = async (req, res) => { export const getInquiry = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const inquiry = await prisma.inquiry.findUnique({ const inquiry = await prisma.inquiry.findUnique({
where: { id: Number(id) }, where: {id: Number(id)},
}); });
if (!inquiry) { if (!inquiry) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Inquiry not found', message: "Inquiry not found",
}); });
} }
@@ -158,7 +158,7 @@ export const getInquiry = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch inquiry', message: "Failed to fetch inquiry",
}); });
} }
}; };
@@ -166,20 +166,20 @@ export const getInquiry = async (req, res) => {
/* DELETE */ /* DELETE */
export const deleteInquiry = async (req, res) => { export const deleteInquiry = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
await prisma.inquiry.delete({ await prisma.inquiry.delete({
where: { id: Number(id) }, where: {id: Number(id)},
}); });
res.json({ res.json({
success: true, success: true,
message: 'Inquiry deleted successfully', message: "Inquiry deleted successfully",
}); });
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to delete inquiry', message: "Failed to delete inquiry",
}); });
} }
}; };
@@ -1,173 +0,0 @@
import prisma from '../prisma/client.js';
export const createInsurancePartner = async (req, res) => {
try {
const { name, logo, websiteUrl, sortOrder, isActive } = req.body;
if (!name || !logo) {
return res.status(400).json({
success: false,
message: 'Name and logo are required',
});
}
const partner = await prisma.insurancePartner.create({
data: {
name,
logo,
websiteUrl,
sortOrder,
isActive,
},
});
res.status(201).json({
success: true,
data: partner,
message: 'Insurance partner created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create insurance partner',
});
}
};
export const getInsurancePartners = async (req, res) => {
try {
const partners = await prisma.insurancePartner.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: partners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partners',
});
}
};
export const getActiveInsurancePartners = async (req, res) => {
try {
const partners = await prisma.insurancePartner.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: partners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partners',
});
}
};
export const getInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
const partner = await prisma.insurancePartner.findUnique({
where: {
id: Number(id),
},
});
if (!partner) {
return res.status(404).json({
success: false,
message: 'Insurance partner not found',
});
}
res.json({
success: true,
data: partner,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partner',
});
}
};
export const updateInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
const { name, logo, websiteUrl, sortOrder, isActive } = req.body;
const partner = await prisma.insurancePartner.update({
where: {
id: Number(id),
},
data: {
name,
logo,
websiteUrl,
sortOrder,
isActive,
},
});
res.json({
success: true,
data: partner,
message: 'Insurance partner updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update insurance partner',
});
}
};
export const deleteInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
await prisma.insurancePartner.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Insurance partner deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete insurance partner',
});
}
};
+23 -15
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// GET ALL NEWS // GET ALL NEWS
@@ -6,7 +6,7 @@ export const getAllNews = async (req, res) => {
try { try {
const page = req.query.page ? parseInt(req.query.page) : null; const page = req.query.page ? parseInt(req.query.page) : null;
const limit = req.query.limit ? parseInt(req.query.limit) : null; const limit = req.query.limit ? parseInt(req.query.limit) : null;
const search = req.query.search?.trim() || ''; const search = req.query.search?.trim() || "";
const includeImages = { const includeImages = {
images: true, images: true,
@@ -16,7 +16,7 @@ export const getAllNews = async (req, res) => {
? { ? {
headline: { headline: {
contains: search, contains: search,
mode: 'insensitive', mode: "insensitive",
}, },
} }
: {}; : {};
@@ -32,7 +32,7 @@ export const getAllNews = async (req, res) => {
prisma.newsMedia.findMany({ prisma.newsMedia.findMany({
where: whereCondition, where: whereCondition,
include: includeImages, include: includeImages,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: "desc" },
skip, skip,
take, take,
}), }),
@@ -69,7 +69,7 @@ export const getAllNews = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to fetch news', message: "Failed to fetch news",
}); });
} }
}; };
@@ -87,7 +87,7 @@ export const getNewsById = async (req, res) => {
if (!n) { if (!n) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'News not found', message: "News not found",
}); });
} }
@@ -113,7 +113,7 @@ export const getNewsById = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to fetch news', message: "Failed to fetch news",
}); });
} }
}; };
@@ -122,12 +122,20 @@ export const getNewsById = async (req, res) => {
export const createNews = async (req, res) => { export const createNews = async (req, res) => {
try { try {
const { headline, content, firstPara, secondPara, date, author, imageUrls } = req.body; const {
headline,
content,
firstPara,
secondPara,
date,
author,
imageUrls,
} = req.body;
if (!headline) { if (!headline) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Headline is required', message: "Headline is required",
}); });
} }
@@ -150,14 +158,14 @@ export const createNews = async (req, res) => {
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: 'News created successfully', message: "News created successfully",
data: news, data: news,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to create news', message: "Failed to create news",
}); });
} }
}; };
@@ -186,14 +194,14 @@ export const updateNews = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'News updated successfully', message: "News updated successfully",
data: news, data: news,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to update news', message: "Failed to update news",
}); });
} }
}; };
@@ -210,13 +218,13 @@ export const deleteNews = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'News deleted successfully', message: "News deleted successfully",
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Failed to delete news', message: "Failed to delete news",
}); });
} }
}; };
+4 -4
View File
@@ -1,9 +1,9 @@
import multer from 'multer'; import multer from "multer";
import path from 'path'; import path from "path";
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: function (req, file, cb) { destination: function (req, file, cb) {
cb(null, 'uploads/blog'); cb(null, "uploads/blog");
}, },
filename: function (req, file, cb) { filename: function (req, file, cb) {
@@ -12,4 +12,4 @@ const storage = multer.diskStorage({
}, },
}); });
export const upload = multer({ storage }); export const upload = multer({storage});
+5 -5
View File
@@ -1,19 +1,19 @@
import { verifyToken } from '../utils/jwt.js'; import {verifyToken} from "../utils/jwt.js";
export default function jwtAuthMiddleware(req, res, next) { export default function jwtAuthMiddleware(req, res, next) {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: 'No token provided' }); return res.status(401).json({error: "No token provided"});
} }
const token = authHeader.split(' ')[1]; const token = authHeader.split(" ")[1];
try { try {
const user = verifyToken(token); const user = verifyToken(token);
req.user = user; req.user = user;
next(); next();
} catch (err) { } catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' }); return res.status(401).json({error: "Invalid or expired token"});
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client'; import {PrismaClient} from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -1,18 +1,18 @@
import express from 'express'; import express from "express";
import { import {
createAcademicsResearch, createAcademicsResearch,
getAcademicsResearch, getAcademicsResearch,
getSingleAcademicsResearch, getSingleAcademicsResearch,
deleteAcademicsResearch, deleteAcademicsResearch,
} from '../controllers/academicsResearch.controller.js'; } from "../controllers/academicsResearch.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.post('/', createAcademicsResearch); router.post("/", createAcademicsResearch);
router.get('/getAll', jwtAuthMiddleware, getAcademicsResearch); router.get("/getAll", jwtAuthMiddleware, getAcademicsResearch);
router.get('/:id', jwtAuthMiddleware, getSingleAcademicsResearch); router.get("/:id", jwtAuthMiddleware, getSingleAcademicsResearch);
router.delete('/:id', jwtAuthMiddleware, deleteAcademicsResearch); router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch);
export default router; export default router;
@@ -1,24 +0,0 @@
import express from 'express';
import {
createAccreditation,
getAccreditations,
getActiveAccreditations,
getAccreditation,
updateAccreditation,
deleteAccreditation,
} from '../controllers/accreditation.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveAccreditations);
router.post('/', jwtAuthMiddleware, createAccreditation);
router.get('/getAll', jwtAuthMiddleware, getAccreditations);
router.get('/:id', jwtAuthMiddleware, getAccreditation);
router.put('/:id', jwtAuthMiddleware, updateAccreditation);
router.delete('/:id', jwtAuthMiddleware, deleteAccreditation);
export default router;
+8 -8
View File
@@ -1,23 +1,23 @@
import express from 'express'; import express from "express";
import { import {
createAppointment, createAppointment,
getAppointments, getAppointments,
getAppointment, getAppointment,
updateAppointment, updateAppointment,
deleteAppointment, deleteAppointment,
} from '../controllers/appointment.controller.js'; } from "../controllers/appointment.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.get('/getall', jwtAuthMiddleware, getAppointments); router.get("/getall", jwtAuthMiddleware, getAppointments);
router.post('/', createAppointment); router.post("/", createAppointment);
router.get('/:id', jwtAuthMiddleware, getAppointment); router.get("/:id", jwtAuthMiddleware, getAppointment);
router.patch('/:id', jwtAuthMiddleware, updateAppointment); router.patch("/:id", jwtAuthMiddleware, updateAppointment);
router.delete('/:id', jwtAuthMiddleware, deleteAppointment); router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
export default router; export default router;
+3 -3
View File
@@ -1,8 +1,8 @@
import express from 'express'; import express from "express";
import { login } from '../controllers/auth.controller.js'; import { login } from "../controllers/auth.controller.js";
const router = express.Router(); const router = express.Router();
router.post('/login', login); router.post("/login", login);
export default router; export default router;
+10 -10
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express from "express";
import { import {
createBlog, createBlog,
getBlogs, getBlogs,
@@ -7,25 +7,25 @@ import {
deleteBlog, deleteBlog,
getAllBlogs, getAllBlogs,
getBlogForAdmin, getBlogForAdmin,
} from '../controllers/blog.controller.js'; } from "../controllers/blog.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.get('/', getBlogs); router.get("/", getBlogs);
router.get('/:slug', getBlog); router.get("/:slug", getBlog);
// Protected // Protected
router.get('/admin/all', jwtAuthMiddleware, getAllBlogs); router.get("/admin/all", jwtAuthMiddleware, getAllBlogs);
router.get('/admin/:id', jwtAuthMiddleware, getBlogForAdmin); router.get("/admin/:id", jwtAuthMiddleware, getBlogForAdmin);
router.post('/', jwtAuthMiddleware, createBlog); router.post("/", jwtAuthMiddleware, createBlog);
router.put('/:id', jwtAuthMiddleware, updateBlog); router.put("/:id", jwtAuthMiddleware, updateBlog);
router.delete('/:id', jwtAuthMiddleware, deleteBlog); router.delete("/:id", jwtAuthMiddleware, deleteBlog);
export default router; export default router;
+9 -9
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express from "express";
import { import {
createCandidate, createCandidate,
getCandidates, getCandidates,
@@ -6,20 +6,20 @@ import {
getCandidatesByCareer, getCandidatesByCareer,
updateCandidate, updateCandidate,
deleteCandidate, deleteCandidate,
} from '../controllers/candidate.controller.js'; } from "../controllers/candidate.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.post('/', createCandidate); router.post("/", createCandidate);
router.get('/getAll', jwtAuthMiddleware, getCandidates); router.get("/getAll", jwtAuthMiddleware, getCandidates);
router.get('/:id', jwtAuthMiddleware, getCandidate); router.get("/:id", jwtAuthMiddleware, getCandidate);
router.get('/career/:careerId', jwtAuthMiddleware, getCandidatesByCareer); router.get("/career/:careerId", jwtAuthMiddleware, getCandidatesByCareer);
router.patch('/:id', jwtAuthMiddleware, updateCandidate); router.patch("/:id", jwtAuthMiddleware, updateCandidate);
router.delete('/:id', jwtAuthMiddleware, deleteCandidate); router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
export default router; export default router;
+12 -7
View File
@@ -1,14 +1,19 @@
import express from 'express'; import express from "express";
import { getAllCareers, createCareer, updateCareer, deleteCareer } from '../controllers/career.controller.js'; import {
getAllCareers,
createCareer,
updateCareer,
deleteCareer,
} from "../controllers/career.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.get('/getAll', getAllCareers); router.get("/getAll", getAllCareers);
router.post('/', jwtAuthMiddleware, createCareer); router.post("/", jwtAuthMiddleware, createCareer);
router.patch('/:id', jwtAuthMiddleware, updateCareer); router.patch("/:id", jwtAuthMiddleware, updateCareer);
router.delete('/:id', jwtAuthMiddleware, deleteCareer); router.delete("/:id", jwtAuthMiddleware, deleteCareer);
export default router; export default router;
+8 -8
View File
@@ -1,22 +1,22 @@
import express from 'express'; import express from "express";
import { import {
getAllDepartments, getAllDepartments,
getDepartmentByName, getDepartmentByName,
createDepartment, createDepartment,
updateDepartment, updateDepartment,
deleteDepartment, deleteDepartment,
} from '../controllers/department.controller.js'; } from "../controllers/department.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
// Public // Public
router.get('/getAll', getAllDepartments); router.get("/getAll", getAllDepartments);
router.get('/search', getDepartmentByName); router.get("/search", getDepartmentByName);
// Protected // Protected
router.post('/', jwtAuthMiddleware, createDepartment); router.post("/", jwtAuthMiddleware, createDepartment);
router.put('/:departmentId', jwtAuthMiddleware, updateDepartment); router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
router.delete('/:departmentId', jwtAuthMiddleware, deleteDepartment); router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
export default router; export default router;
+11 -13
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express from "express";
import { import {
getAllDoctors, getAllDoctors,
createDoctor, createDoctor,
@@ -8,22 +8,20 @@ import {
getDoctorTimingById, getDoctorTimingById,
getDoctorByDoctorId, getDoctorByDoctorId,
getDoctorsByDepartmentId, getDoctorsByDepartmentId,
getFeaturedDoctors, } from "../controllers/doctor.controller.js";
} from '../controllers/doctor.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.get('/getAll', getAllDoctors); router.get("/getAll", getAllDoctors);
router.get('/search', getDoctorsByDepartmentId); router.get("/search", getDoctorsByDepartmentId);
router.get('/getTimings', getDoctorTimings); router.get("/getTimings", getDoctorTimings);
router.get('/getTimings/:doctorId', getDoctorTimingById); router.get("/getTimings/:doctorId", getDoctorTimingById);
router.get('/featured', getFeaturedDoctors); router.get("/:doctorId", getDoctorByDoctorId);
router.get('/:doctorId', getDoctorByDoctorId);
router.post('/', jwtAuthMiddleware, createDoctor); router.post("/", jwtAuthMiddleware, createDoctor);
router.patch('/:doctorId/:action', jwtAuthMiddleware, updateDoctor); router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor);
router.delete('/:doctorId', jwtAuthMiddleware, deleteDoctor); router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
export default router; export default router;
+7 -7
View File
@@ -1,19 +1,19 @@
import express from 'express'; import express from "express";
import { import {
getEmailConfigs, getEmailConfigs,
createEmailConfig, createEmailConfig,
updateEmailConfig, updateEmailConfig,
deleteEmailConfig, deleteEmailConfig,
} from '../controllers/emailConfig.controller.js'; } from "../controllers/emailConfig.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.get('/getAll', getEmailConfigs); router.get("/getAll", getEmailConfigs);
router.post('/', jwtAuthMiddleware, createEmailConfig); router.post("/", jwtAuthMiddleware, createEmailConfig);
router.patch('/:id', jwtAuthMiddleware, updateEmailConfig); router.patch("/:id", jwtAuthMiddleware, updateEmailConfig);
router.delete('/:id', jwtAuthMiddleware, deleteEmailConfig); router.delete("/:id", jwtAuthMiddleware, deleteEmailConfig);
export default router; export default router;
-41
View File
@@ -1,41 +0,0 @@
import express from 'express';
import {
// Categories
getAllCategories,
getPackageBySlug,
createCategory,
updateCategory,
deleteCategory,
// Packages
getAllPackages,
createPackage,
updatePackage,
deletePackage,
getFeaturedPackages,
// Inquiries
createPackageInquiry,
getAllInquiries,
} from '../controllers/healthCheck.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/packages', getAllPackages);
router.get('/packages/:slug', getPackageBySlug);
router.get('/categories', getAllCategories);
router.post('/inquiry', createPackageInquiry);
router.get('/featured', getFeaturedPackages);
router.get('/inquiries', jwtAuthMiddleware, getAllInquiries);
router.post('/', jwtAuthMiddleware, createPackage);
router.patch('/:id', jwtAuthMiddleware, updatePackage);
router.delete('/:id', jwtAuthMiddleware, deletePackage);
router.post('/categories', jwtAuthMiddleware, createCategory);
router.patch('/categories/:id', jwtAuthMiddleware, updateCategory);
router.delete('/categories/:id', jwtAuthMiddleware, deleteCategory);
export default router;
@@ -1,27 +0,0 @@
import express from 'express';
import {
createHomepageBanner,
getHomepageBanners,
getActiveHomepageBanners,
getHomepageBanner,
updateHomepageBanner,
deleteHomepageBanner,
} from '../controllers/homepageBanner.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveHomepageBanners);
router.post('/', jwtAuthMiddleware, createHomepageBanner);
router.get('/getAll', jwtAuthMiddleware, getHomepageBanners);
router.get('/:id', jwtAuthMiddleware, getHomepageBanner);
router.put('/:id', jwtAuthMiddleware, updateHomepageBanner);
router.delete('/:id', jwtAuthMiddleware, deleteHomepageBanner);
export default router;
+4 -4
View File
@@ -1,9 +1,9 @@
import express from 'express'; import express from "express";
import { bulkImportExcelData } from '../controllers/importController.js'; import { bulkImportExcelData } from "../controllers/importController.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.post('/bulk', jwtAuthMiddleware, bulkImportExcelData); router.post("/bulk", jwtAuthMiddleware, bulkImportExcelData);
export default router; export default router;
+12 -7
View File
@@ -1,14 +1,19 @@
import express from 'express'; import express from "express";
import { createInquiry, getInquiries, getInquiry, deleteInquiry } from '../controllers/inquiry.controller.js'; import {
createInquiry,
getInquiries,
getInquiry,
deleteInquiry,
} from "../controllers/inquiry.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.post('/', createInquiry); router.post("/", createInquiry);
router.get('/getAll', jwtAuthMiddleware, getInquiries); router.get("/getAll", jwtAuthMiddleware, getInquiries);
router.get('/:id', jwtAuthMiddleware, getInquiry); router.get("/:id", jwtAuthMiddleware, getInquiry);
router.delete('/:id', jwtAuthMiddleware, deleteInquiry); router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
export default router; export default router;
@@ -1,28 +0,0 @@
import express from 'express';
import {
createInsurancePartner,
getInsurancePartners,
getActiveInsurancePartners,
getInsurancePartner,
updateInsurancePartner,
deleteInsurancePartner,
} from '../controllers/insurancePartner.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveInsurancePartners);
router.post('/', jwtAuthMiddleware, createInsurancePartner);
router.get('/getAll', jwtAuthMiddleware, getInsurancePartners);
router.get('/:id', jwtAuthMiddleware, getInsurancePartner);
router.put('/:id', jwtAuthMiddleware, updateInsurancePartner);
router.delete('/:id', jwtAuthMiddleware, deleteInsurancePartner);
export default router;
+14 -8
View File
@@ -1,17 +1,23 @@
import express from 'express'; import express from "express";
import { createNews, getAllNews, getNewsById, updateNews, deleteNews } from '../controllers/newsMedia.controller.js'; import {
createNews,
getAllNews,
getNewsById,
updateNews,
deleteNews,
} from "../controllers/newsMedia.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
// PUBLIC ROUTES // PUBLIC ROUTES
router.get('/getAll', getAllNews); router.get("/getAll", getAllNews);
router.get('/:id', getNewsById); router.get("/:id", getNewsById);
// PROTECTED ROUTES // PROTECTED ROUTES
router.post('/', jwtAuthMiddleware, createNews); router.post("/", jwtAuthMiddleware, createNews);
router.patch('/:id', jwtAuthMiddleware, updateNews); router.patch("/:id", jwtAuthMiddleware, updateNews);
router.delete('/:id', jwtAuthMiddleware, deleteNews); router.delete("/:id", jwtAuthMiddleware, deleteNews);
export default router; export default router;
+9 -9
View File
@@ -1,6 +1,6 @@
import express from 'express'; import express from "express";
import * as Bytescale from '@bytescale/sdk'; import * as Bytescale from "@bytescale/sdk";
import multer from 'multer'; import multer from "multer";
const router = express.Router(); const router = express.Router();
@@ -9,26 +9,26 @@ const uploadManager = new Bytescale.UploadManager({
}); });
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();
const upload = multer({ storage }); const upload = multer({storage});
router.post('/', upload.single('file'), async (req, res) => { router.post("/", upload.single("file"), async (req, res) => {
try { try {
const file = req.file; const file = req.file;
const { folderPath } = req.body; const {folderPath} = req.body;
const result = await uploadManager.upload({ const result = await uploadManager.upload({
data: file.buffer, data: file.buffer,
name: file.originalname, name: file.originalname,
mime: file.mimetype, mime: file.mimetype,
path: { path: {
folderPath: folderPath || '/general', folderPath: folderPath || "/general",
}, },
}); });
res.json({ fileUrl: result.fileUrl }); res.json({fileUrl: result.fileUrl});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ error: 'Upload failed' }); res.status(500).json({error: "Upload failed"});
} }
}); });
+8 -6
View File
@@ -1,12 +1,14 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { hashPassword } from './password.js'; import { hashPassword } from "./password.js";
async function main() { async function main() {
const username = process.argv[2]; const username = process.argv[2];
const password = process.argv[3]; const password = process.argv[3];
const role = process.argv[4] || 'admin'; const role = process.argv[4] || "admin";
if (!username || !password) { if (!username || !password) {
console.log('Usage: node scripts/createUser.js <username> <password> [role]'); console.log(
"Usage: node scripts/createUser.js <username> <password> [role]",
);
process.exit(1); process.exit(1);
} }
@@ -15,7 +17,7 @@ async function main() {
}); });
if (existingUser) { if (existingUser) {
console.log('User already exists'); console.log("User already exists");
process.exit(1); process.exit(1);
} }
@@ -29,7 +31,7 @@ async function main() {
}, },
}); });
console.log('User created:', { console.log("User created:", {
id: user.id, id: user.id,
username: user.username, username: user.username,
role: user.role, role: user.role,
+4 -4
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
export const getEmailsByType = async (type) => { export const getEmailsByType = async (type) => {
try { try {
@@ -9,9 +9,9 @@ export const getEmailsByType = async (type) => {
}, },
}); });
return emails.map((e) => e.email).join(','); return emails.map((e) => e.email).join(",");
} catch (error) { } catch (error) {
console.error('Fetch email config error:', error); console.error("Fetch email config error:", error);
return ''; return "";
} }
}; };
+3 -3
View File
@@ -1,10 +1,10 @@
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import 'dotenv/config'; import "dotenv/config";
const SECRET = process.env.JWT_SECRET; const SECRET = process.env.JWT_SECRET;
export function generateToken(payload) { export function generateToken(payload) {
return jwt.sign(payload, SECRET, { expiresIn: '24h' }); return jwt.sign(payload, SECRET, {expiresIn: "24h"});
} }
export function verifyToken(token) { export function verifyToken(token) {
+1 -1
View File
@@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import bcrypt from "bcryptjs";
export async function hashPassword(password) { export async function hashPassword(password) {
return bcrypt.hash(password, 10); return bcrypt.hash(password, 10);
+5 -5
View File
@@ -1,18 +1,18 @@
import postmark from 'postmark'; import postmark from "postmark";
const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY); const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
export const sendEmail = async ({ to, subject, html, text }) => { export const sendEmail = async ({to, subject, html, text}) => {
try { try {
await client.sendEmail({ await client.sendEmail({
From: process.env.EMAIL_FROM, From: process.env.EMAIL_FROM,
To: to, To: to,
Subject: subject, Subject: subject,
HtmlBody: html, HtmlBody: html,
TextBody: text || '', TextBody: text || "",
MessageStream: 'outbound', MessageStream: "outbound",
}); });
} catch (error) { } catch (error) {
console.error('Email send error:', error); console.error("Email send error:", error);
} }
}; };
+39 -39
View File
@@ -1,45 +1,45 @@
version: '3.8' version: "3.8"
services: services:
backend: backend:
build: build:
context: . context: .
dockerfile: docker/dev/Dockerfile.main dockerfile: docker/dev/Dockerfile.main
ports: ports:
- '127.0.0.1:5008:5008' - "127.0.0.1:5008:5008"
env_file: env_file:
- ./backend/.env - ./backend/.env
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
frontend: frontend:
build: build:
context: . context: .
dockerfile: docker/dev/Dockerfile.frontend dockerfile: docker/dev/Dockerfile.frontend
ports: ports:
- '127.0.0.1:3008:3000' - "127.0.0.1:3008:3000"
env_file: env_file:
- ./frontend/.env - ./frontend/.env
restart: unless-stopped restart: unless-stopped
db: db:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
- POSTGRES_USER=${POSTGRES_USER} - POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB} - POSTGRES_DB=${POSTGRES_DB}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:
external: true external: true
name: gg-backend_postgres_data name: gg-backend_postgres_data
+36 -36
View File
@@ -1,41 +1,41 @@
services: services:
backend: backend:
build: build:
context: . context: .
dockerfile: docker/dev/Dockerfile.main dockerfile: docker/dev/Dockerfile.main
ports: ports:
- '127.0.0.1:5008:5008' - "127.0.0.1:5008:5008"
env_file: env_file:
- ./backend/.env - ./backend/.env
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
restart: always restart: always
frontend: frontend:
build: build:
context: . context: .
dockerfile: docker/dev/Dockerfile.frontend dockerfile: docker/dev/Dockerfile.frontend
ports: ports:
- '127.0.0.1:3008:3000' - "127.0.0.1:3008:3000"
env_file: env_file:
- ./frontend/.env - ./frontend/.env
restart: always restart: always
db: db:
image: postgres:16-alpine image: postgres:16-alpine
environment: environment:
- POSTGRES_USER=${POSTGRES_USER} - POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB} - POSTGRES_DB=${POSTGRES_DB}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: always restart: always
volumes: volumes:
postgres_data: postgres_data:
+4 -2
View File
@@ -4,8 +4,10 @@ set -e # Exit immediately if a command exits with a non-zero status
echo "Generating Prisma Client..." echo "Generating Prisma Client..."
npx prisma generate npx prisma generate
echo "Running migrate..." # echo "Running migrate..."
npx prisma migrate deploy # npx prisma migrate deploy
echo "Running PUSH..."
npx prisma db push
echo "Executing command: $@" echo "Executing command: $@"
exec "$@" exec "$@"
+23 -23
View File
@@ -1,25 +1,25 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova", "style": "radix-nova",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/index.css", "css": "src/index.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"iconLibrary": "lucide", "iconLibrary": "lucide",
"rtl": false, "rtl": false,
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
}, },
"menuColor": "default", "menuColor": "default",
"menuAccent": "subtle", "menuAccent": "subtle",
"registries": {} "registries": {}
} }
+21 -21
View File
@@ -1,23 +1,23 @@
import js from '@eslint/js'; import js from '@eslint/js'
import globals from 'globals'; import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'; import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'; import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(['dist']),
{ {
files: ['**/*.{ts,tsx}'], files: ['**/*.{ts,tsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
reactHooks.configs.flat.recommended, reactHooks.configs.flat.recommended,
reactRefresh.configs.vite, reactRefresh.configs.vite,
], ],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
}, },
}, },
]); ])
+9697 -9697
View File
File diff suppressed because it is too large Load Diff
+57 -57
View File
@@ -1,59 +1,59 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@editorjs/code": "^2.9.4", "@editorjs/code": "^2.9.4",
"@editorjs/delimiter": "^1.4.2", "@editorjs/delimiter": "^1.4.2",
"@editorjs/editorjs": "^2.31.5", "@editorjs/editorjs": "^2.31.5",
"@editorjs/embed": "^2.8.0", "@editorjs/embed": "^2.8.0",
"@editorjs/header": "^2.8.8", "@editorjs/header": "^2.8.8",
"@editorjs/image": "^2.10.3", "@editorjs/image": "^2.10.3",
"@editorjs/list": "^2.0.9", "@editorjs/list": "^2.0.9",
"@editorjs/quote": "^2.7.6", "@editorjs/quote": "^2.7.6",
"@editorjs/table": "^2.4.5", "@editorjs/table": "^2.4.5",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@tailwindcss/postcss": "^4.2.1", "@tailwindcss/postcss": "^4.2.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/estree": "^1.0.8", "@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.48.0", "typescript-eslint": "^8.48.0",
"vite": "^7.3.1" "vite": "^7.3.1"
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, "@tailwindcss/postcss": {},
}, },
}; };
+20 -28
View File
@@ -1,32 +1,28 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import { Toaster } from 'react-hot-toast'; import {Toaster} from "react-hot-toast";
import Login from '@/pages/Login'; import Login from "@/pages/Login";
import DashboardLayout from './layouts/DashboardLayout'; import DashboardLayout from "./layouts/DashboardLayout";
// import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes"; // import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes";
import ProtectedRoute from './auth/ProtectedRoute'; import ProtectedRoute from "./auth/ProtectedRoute";
import PublicRoute from './auth/PublicRoute'; import PublicRoute from "./auth/PublicRoute";
import { AuthProvider } from './context/AuthContext'; import {AuthProvider} from "./context/AuthContext";
import Department from './pages/Department'; import Department from "./pages/Department";
import Doctor from './pages/Doctor'; import Doctor from "./pages/Doctor";
import Blog from './pages/Blog'; import Blog from "./pages/Blog";
import BlogEditorPage from './pages/BlogEditor'; import BlogEditorPage from "./pages/BlogEditor";
import Appointment from './pages/Appointment'; import Appointment from "./pages/Appointment";
import EmailPage from './pages/email'; import EmailPage from "./pages/email";
import CareerPage from './pages/Career'; import CareerPage from "./pages/Career";
import CandidatePage from './pages/candidates'; import CandidatePage from "./pages/candidates";
import InquiryPage from './pages/inquiry'; import InquiryPage from "./pages/inquiry";
import AcademicsPage from './pages/Academics'; import AcademicsPage from "./pages/Academics";
import NewsPage from './pages/newsMedia'; import NewsPage from "./pages/newsMedia";
import BlogDetail from './pages/BlogDetails'; import BlogDetail from "./pages/BlogDetails";
import ImportData from './pages/ImportData'; import ImportData from "./pages/ImportData";
import HealthPackagePage from './pages/HealthPackagePage';
import HomepageBanner from './pages/HomepageBannerPage';
import InsurancePartnerPage from './pages/InsurancePartner';
import AccreditationPage from './pages/Accreditation';
export default function App() { export default function App() {
return ( return (
@@ -55,10 +51,6 @@ export default function App() {
<Route path="/academics" element={<AcademicsPage />} /> <Route path="/academics" element={<AcademicsPage />} />
<Route path="/news" element={<NewsPage />} /> <Route path="/news" element={<NewsPage />} />
<Route path="/import" element={<ImportData />} /> <Route path="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} />
<Route path="/homepage-banner" element={<HomepageBanner />} />
<Route path="/insurance-partner" element={<InsurancePartnerPage />} />
<Route path="/accreditation" element={<AccreditationPage />} />
</Route> </Route>
</Route> </Route>
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getAcademicsApi = async () => { export const getAcademicsApi = async () => {
const res = await apiClient.get('/academics/getAll'); const res = await apiClient.get("/academics/getAll");
return res.data; return res.data;
}; };
-85
View File
@@ -1,85 +0,0 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export type AccreditationType = 'ACCREDITATION' | 'CERTIFICATION';
export interface Accreditation {
id?: number;
title: string;
type: AccreditationType;
logo?: string;
image?: string;
description?: string;
sortOrder: number;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
}
export const getAccreditationsApi = async (type?: AccreditationType) => {
const query = type ? `?type=${type}` : '';
const res = await apiClient.get(`/accreditation/getAll${query}`);
return res.data;
};
export const getAccreditationApi = async (id: number) => {
const res = await apiClient.get(`/accreditation/${id}`);
return res.data;
};
export const getActiveAccreditationsApi = async (type?: AccreditationType) => {
const query = type ? `?type=${type}` : '';
const res = await apiClient.get(`/accreditation/active${query}`);
return res.data;
};
export const createAccreditationApi = async (data: Partial<Accreditation>) => {
try {
const res = await apiClient.post('/accreditation', data);
toast.success('Accreditation created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create accreditation');
throw error;
}
};
export const updateAccreditationApi = async (id: number, data: Partial<Accreditation>) => {
try {
const res = await apiClient.put(`/accreditation/${id}`, data);
toast.success('Accreditation updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update accreditation');
throw error;
}
};
export const deleteAccreditationApi = async (id: number) => {
try {
const res = await apiClient.delete(`/accreditation/${id}`);
toast.success('Accreditation deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete accreditation');
throw error;
}
};
+5 -5
View File
@@ -1,12 +1,12 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getAppointmentsApi = async ( export const getAppointmentsApi = async (
page = 1, page = 1,
limit = 10, limit = 10,
date = '', date = "",
startDate = '', startDate = "",
endDate = '', endDate = "",
search = '' search = "",
) => { ) => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page: String(page), page: String(page),
+6 -3
View File
@@ -1,7 +1,10 @@
import apiClient from './client'; import apiClient from "./client";
export const loginApi = async (username: string, password: string): Promise<any> => { export const loginApi = async (
const response = await apiClient.post('/auth/login/', { username: string,
password: string,
): Promise<any> => {
const response = await apiClient.post("/auth/login/", {
username, username,
password, password,
}); });
+6 -6
View File
@@ -1,4 +1,4 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export interface Blog { export interface Blog {
id?: number; id?: number;
@@ -9,7 +9,7 @@ export interface Blog {
} }
export const getAllBlogsApi = async () => { export const getAllBlogsApi = async () => {
const res = await apiClient.get('/blogs'); const res = await apiClient.get("/blogs");
return res.data; return res.data;
}; };
@@ -19,7 +19,7 @@ export const getBlogByIdApi = async (id: number) => {
}; };
export const createBlogApi = async (data: Blog) => { export const createBlogApi = async (data: Blog) => {
const res = await apiClient.post('/blogs', data); const res = await apiClient.post("/blogs", data);
return res.data; return res.data;
}; };
@@ -36,11 +36,11 @@ export const deleteBlogApi = async (id: number) => {
/* IMAGE UPLOAD */ /* IMAGE UPLOAD */
export const uploadImageApi = async (file: File) => { export const uploadImageApi = async (file: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('image', file); formData.append("image", file);
const res = await apiClient.post('/upload/image', formData, { const res = await apiClient.post("/upload/image", formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', "Content-Type": "multipart/form-data",
}, },
}); });
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getCandidatesApi = async () => { export const getCandidatesApi = async () => {
const res = await apiClient.get('/candidates/getAll'); const res = await apiClient.get("/candidates/getAll");
return res.data; return res.data;
}; };
+10 -10
View File
@@ -1,20 +1,20 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export const getCareersApi = async () => { export const getCareersApi = async () => {
const res = await apiClient.get('/careers/getAll?admin=true'); const res = await apiClient.get("/careers/getAll?admin=true");
return res.data; return res.data;
}; };
export const createCareerApi = async (data: any) => { export const createCareerApi = async (data: any) => {
try { try {
const res = await apiClient.post('/careers', data); const res = await apiClient.post("/careers", data);
toast.success('Career created successfully'); toast.success("Career created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create career'); toast.error(error?.response?.data?.message || "Failed to create career");
throw error; throw error;
} }
@@ -24,11 +24,11 @@ export const updateCareerApi = async (id: number, data: any) => {
try { try {
const res = await apiClient.patch(`/careers/${id}`, data); const res = await apiClient.patch(`/careers/${id}`, data);
toast.success('Career updated successfully'); toast.success("Career updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update career'); toast.error(error?.response?.data?.message || "Failed to update career");
throw error; throw error;
} }
@@ -38,11 +38,11 @@ export const deleteCareerApi = async (id: number) => {
try { try {
const res = await apiClient.delete(`/careers/${id}`); const res = await apiClient.delete(`/careers/${id}`);
toast.success('Career deleted successfully'); toast.success("Career deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete career'); toast.error(error?.response?.data?.message || "Failed to delete career");
throw error; throw error;
} }
+12 -12
View File
@@ -1,48 +1,48 @@
import axios from 'axios'; import axios from "axios";
import type { InternalAxiosRequestConfig } from 'axios'; import type {InternalAxiosRequestConfig} from "axios";
const baseURL: string = import.meta.env.VITE_API_URL; const baseURL: string = import.meta.env.VITE_API_URL;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: baseURL, baseURL: baseURL,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
}); });
export const setAxiosAuthToken = (token: string | null): void => { export const setAxiosAuthToken = (token: string | null): void => {
if (token) { if (token) {
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else { } else {
delete apiClient.defaults.headers.common['Authorization']; delete apiClient.defaults.headers.common["Authorization"];
} }
}; };
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem("token");
if (token && config.headers) { if (token && config.headers) {
config.headers['Authorization'] = `Bearer ${token}`; config.headers["Authorization"] = `Bearer ${token}`;
} }
return config; return config;
}, },
(error: any) => Promise.reject(error) (error: any) => Promise.reject(error),
); );
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('Unauthorized - token missing or invalid'); console.error("Unauthorized - token missing or invalid");
localStorage.removeItem('token'); localStorage.removeItem("token");
window.location.href = '/login'; window.location.href = "/login";
} }
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default apiClient; export default apiClient;
+17 -11
View File
@@ -1,5 +1,5 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface Department { export interface Department {
departmentId: string; departmentId: string;
@@ -15,7 +15,7 @@ export interface Department {
} }
export const getDepartmentsApi = async () => { export const getDepartmentsApi = async () => {
const res = await apiClient.get('/departments/getAll?admin=true'); const res = await apiClient.get("/departments/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -29,13 +29,15 @@ export const createDepartmentApi = async (data: {
services?: string; services?: string;
}) => { }) => {
try { try {
const res = await apiClient.post('/departments', data); const res = await apiClient.post("/departments", data);
toast.success('Department created successfully'); toast.success("Department created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create department'); toast.error(
error?.response?.data?.message || "Failed to create department",
);
throw error; throw error;
} }
@@ -50,16 +52,18 @@ export const updateDepartmentApi = async (
para3?: string; para3?: string;
facilities?: string; facilities?: string;
services?: string; services?: string;
} },
) => { ) => {
try { try {
const res = await apiClient.put(`/departments/${departmentId}`, data); const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success('Department updated successfully'); toast.success("Department updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update department'); toast.error(
error?.response?.data?.message || "Failed to update department",
);
throw error; throw error;
} }
@@ -69,11 +73,13 @@ export const deleteDepartmentApi = async (departmentId: string) => {
try { try {
const res = await apiClient.delete(`/departments/${departmentId}`); const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success('Department deleted successfully'); toast.success("Department deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete department'); toast.error(
error?.response?.data?.message || "Failed to delete department",
);
throw error; throw error;
} }
+11 -13
View File
@@ -1,5 +1,5 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface Doctor { export interface Doctor {
doctorId: string; doctorId: string;
@@ -9,7 +9,6 @@ export interface Doctor {
workingStatus?: string; workingStatus?: string;
qualification?: string; qualification?: string;
isActive: boolean; isActive: boolean;
isFeatured: boolean;
globalSortOrder: number; globalSortOrder: number;
departments?: { departments?: {
@@ -28,7 +27,7 @@ export interface Doctor {
} }
export const getDoctorsApi = async () => { export const getDoctorsApi = async () => {
const res = await apiClient.get('/doctors/getAll?admin=true'); const res = await apiClient.get("/doctors/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -39,13 +38,13 @@ export const getDoctorByIdApi = async (doctorId: string) => {
export const createDoctorApi = async (data: Doctor) => { export const createDoctorApi = async (data: Doctor) => {
try { try {
const res = await apiClient.post('/doctors', data); const res = await apiClient.post("/doctors", data);
toast.success('Doctor created successfully'); toast.success("Doctor created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create doctor'); toast.error(error?.response?.data?.message || "Failed to create doctor");
throw error; throw error;
} }
@@ -54,16 +53,15 @@ export const createDoctorApi = async (data: Doctor) => {
export const updateDoctorApi = async ( export const updateDoctorApi = async (
doctorId: string, doctorId: string,
data: Partial<Doctor>, data: Partial<Doctor>,
action: 'toggleStatus' | 'toggleFeatured' | 'updateDetails' = 'updateDetails'
) => { ) => {
try { try {
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data); const res = await apiClient.patch(`/doctors/${doctorId}`, data);
toast.success('Doctor updated successfully'); toast.success("Doctor updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update doctor'); toast.error(error?.response?.data?.message || "Failed to update doctor");
throw error; throw error;
} }
@@ -73,11 +71,11 @@ export const deleteDoctorApi = async (doctorId: string) => {
try { try {
const res = await apiClient.delete(`/doctors/${doctorId}`); const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success('Doctor deleted successfully'); toast.success("Doctor deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete doctor'); toast.error(error?.response?.data?.message || "Failed to delete doctor");
throw error; throw error;
} }
+7 -4
View File
@@ -1,4 +1,4 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export interface EmailConfig { export interface EmailConfig {
id?: number; id?: number;
@@ -10,18 +10,21 @@ export interface EmailConfig {
// GET ALL // GET ALL
export const getEmailConfigsApi = async () => { export const getEmailConfigsApi = async () => {
const res = await apiClient.get('/email/getAll'); const res = await apiClient.get("/email/getAll");
return res.data; return res.data;
}; };
// CREATE // CREATE
export const createEmailConfigApi = async (data: EmailConfig) => { export const createEmailConfigApi = async (data: EmailConfig) => {
const res = await apiClient.post('/email', data); const res = await apiClient.post("/email", data);
return res.data; return res.data;
}; };
// UPDATE // UPDATE
export const updateEmailConfigApi = async (id: number, data: Partial<EmailConfig>) => { export const updateEmailConfigApi = async (
id: number,
data: Partial<EmailConfig>,
) => {
const res = await apiClient.patch(`/email/${id}`, data); const res = await apiClient.patch(`/email/${id}`, data);
return res.data; return res.data;
}; };
-155
View File
@@ -1,155 +0,0 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
export interface HealthPackage {
id?: number;
name: string;
slug: string;
description?: string;
price?: number;
image?: string;
discountedPrice?: number;
inclusions: Record<string, string[]>;
categoryId: number;
isActive: boolean;
isFeatured: boolean;
sortOrder: number;
seo?: SeoData | null;
category?: {
name: string;
};
}
export interface HealthCategory {
id: number;
name: string;
slug: string;
sortOrder: number;
isActive: boolean;
}
export interface HealthInquiry {
id: number;
fullName: string;
mobileNumber: string;
email?: string;
age: string;
gender: string;
preferredDate: string;
message?: string;
createdAt: string;
healthPackage?: {
name: string;
category?: {
name: string;
};
};
}
export const getHealthCategoriesApi = async () => {
const res = await apiClient.get('/health-check/categories?admin=true');
return res.data;
};
export const getHealthPackagesApi = async () => {
const res = await apiClient.get('/health-check/packages?admin=true');
return res.data;
};
export const createHealthPackageApi = async (data: Partial<HealthPackage>) => {
try {
const res = await apiClient.post('/health-check', data);
toast.success('Package created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create package');
throw error;
}
};
export const updateHealthPackageApi = async (id: number, data: Partial<HealthPackage>) => {
try {
const res = await apiClient.patch(`/health-check/${id}`, data);
toast.success('Package updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update package');
throw error;
}
};
export const deleteHealthPackageApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/${id}`);
toast.success('Package deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete package');
throw error;
}
};
export const createCategoryApi = async (data: { name: string; slug: string; sortOrder: number }) => {
try {
const res = await apiClient.post('/health-check/categories', data);
toast.success('Category created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create category');
throw error;
}
};
export const updateCategoryApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/health-check/categories/${id}`, data);
toast.success('Category updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update category');
throw error;
}
};
export const deleteCategoryApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/categories/${id}`);
toast.success('Category deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete category');
throw error;
}
};
export const getAllInquiriesApi = async (page = 1, limit = 10, filterDate = '', startDate = '', endDate = '') => {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (filterDate) params.append('filterDate', filterDate);
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
const res = await apiClient.get(`/health-check/inquiries?${params.toString()}`);
return res.data;
};
-83
View File
@@ -1,83 +0,0 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export type BannerMediaType = 'IMAGE' | 'VIDEO';
export interface HomepageBanner {
id?: number;
title?: string;
subtitle?: string;
mediaType: BannerMediaType;
desktopMediaUrl: string;
mobileMediaUrl?: string;
buttonText?: string;
buttonLink?: string;
openInNewTab: boolean;
textAlignment?: 'left' | 'center' | 'right';
sortOrder: number;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
}
export const getHomepageBannersApi = async () => {
const res = await apiClient.get('/homepage-banners/getAll');
return res.data;
};
export const getHomepageBannerApi = async (id: number) => {
const res = await apiClient.get(`/homepage-banners/${id}`);
return res.data;
};
export const getActiveHomepageBannersApi = async () => {
const res = await apiClient.get('/homepage-banners/active');
return res.data;
};
export const createHomepageBannerApi = async (data: Partial<HomepageBanner>) => {
try {
const res = await apiClient.post('/homepage-banners', data);
toast.success('Banner created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create banner');
throw error;
}
};
export const updateHomepageBannerApi = async (id: number, data: Partial<HomepageBanner>) => {
try {
const res = await apiClient.put(`/homepage-banners/${id}`, data);
toast.success('Banner updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update banner');
throw error;
}
};
export const deleteHomepageBannerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/homepage-banners/${id}`);
toast.success('Banner deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete banner');
throw error;
}
};
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getInquiriesApi = async () => { export const getInquiriesApi = async () => {
const res = await apiClient.get('/inquiry/getAll'); const res = await apiClient.get("/inquiry/getAll");
return res.data; return res.data;
}; };
-61
View File
@@ -1,61 +0,0 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface InsurancePartner {
id?: number;
name: string;
logo: string;
websiteUrl?: string;
sortOrder: number;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
}
export const getInsurancePartnersApi = async () => {
const res = await apiClient.get('/insurance-partners/getAll');
return res.data;
};
export const getInsurancePartnerApi = async (id: number) => {
const res = await apiClient.get(`/insurance-partners/${id}`);
return res.data;
};
export const getActiveInsurancePartnersApi = async () => {
const res = await apiClient.get('/insurance-partners/active');
return res.data;
};
export const createInsurancePartnerApi = async (data: Partial<InsurancePartner>) => {
try {
const res = await apiClient.post('/insurance-partners', data);
toast.success('Insurance partner created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create insurance partner');
throw error;
}
};
export const updateInsurancePartnerApi = async (id: number, data: Partial<InsurancePartner>) => {
try {
const res = await apiClient.put(`/insurance-partners/${id}`, data);
toast.success('Insurance partner updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update insurance partner');
throw error;
}
};
export const deleteInsurancePartnerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/insurance-partners/${id}`);
toast.success('Insurance partner deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete insurance partner');
throw error;
}
};
+6 -4
View File
@@ -1,12 +1,14 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getNewsApi = async (page = 1, limit = 10, search = '') => { export const getNewsApi = async (page = 1, limit = 10, search = "") => {
const res = await apiClient.get(`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`); const res = await apiClient.get(
`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`,
);
return res.data; return res.data;
}; };
export const createNewsApi = async (data: any) => { export const createNewsApi = async (data: any) => {
const res = await apiClient.post('/newsMedia', data); const res = await apiClient.post("/newsMedia", data);
return res.data; return res.data;
}; };
+3 -3
View File
@@ -1,7 +1,7 @@
import { Navigate, Outlet } from 'react-router-dom'; import {Navigate, Outlet} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute() { export default function ProtectedRoute() {
const { token } = useAuth(); const {token} = useAuth();
return token ? <Outlet /> : <Navigate to="/" replace />; return token ? <Outlet /> : <Navigate to="/" replace />;
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { Navigate, Outlet } from 'react-router-dom'; import {Navigate, Outlet} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function PublicRoute() { export default function PublicRoute() {
const { token } = useAuth(); const {token} = useAuth();
return token ? <Navigate to="/dashboard" replace /> : <Outlet />; return token ? <Navigate to="/dashboard" replace /> : <Outlet />;
} }
@@ -1,209 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingAccreditation: any;
accreditationForm: any;
setAccreditationForm: any;
onSave: () => void;
}
export default function AccreditationModal({
open,
onOpenChange,
editingAccreditation,
accreditationForm,
setAccreditationForm,
onSave,
}: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-3xl h-auto max-h-[90vh] flex flex-col p-0 overflow-hidden">
{/* Header */}
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingAccreditation ? 'Edit Accreditation / Certification' : 'Create Accreditation / Certification'}
</DialogTitle>
</DialogHeader>
{/* Body */}
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{/* Basic Information */}
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Basic Information</h3>
<p className="text-sm text-muted-foreground">Configure accreditation or certification details.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<Label className="font-semibold">Category</Label>
<Select
value={accreditationForm.type}
onValueChange={(value) =>
setAccreditationForm({
...accreditationForm,
type: value,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ACCREDITATION">Accreditation</SelectItem>
<SelectItem value="CERTIFICATION">Certification</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="font-semibold">Title</Label>
<Input
value={accreditationForm.title || ''}
placeholder="e.g. NABH, NABL, ISO 9001"
onChange={(e) =>
setAccreditationForm({
...accreditationForm,
title: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Description (Optional)</Label>
<Textarea
rows={4}
value={accreditationForm.description || ''}
placeholder="Short description about this accreditation or certificate"
onChange={(e) =>
setAccreditationForm({
...accreditationForm,
description: e.target.value,
})
}
/>
</div>
</div>
{/* Media Upload */}
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Media Assets</h3>
<p className="text-sm text-muted-foreground">Upload logo and certificate images.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="font-semibold">Logo (Optional)</Label>
<p className="text-xs text-muted-foreground">Recommended transparent PNG/SVG logo.</p>
<BytescaleUploader
value={accreditationForm.logo || ''}
folderPath="/accreditations"
onChange={(url) =>
setAccreditationForm({
...accreditationForm,
logo: url,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Certificate Image (Optional)</Label>
<p className="text-xs text-muted-foreground">Upload certificate, award or recognition image.</p>
<BytescaleUploader
value={accreditationForm.image || ''}
folderPath="/accreditations"
onChange={(url) =>
setAccreditationForm({
...accreditationForm,
image: url,
})
}
/>
</div>
</div>
</div>
{/* Display Settings */}
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Display Settings</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-2">
<Label className="font-semibold">Priority</Label>
<Input
type="number"
value={accreditationForm.sortOrder}
onChange={(e) =>
setAccreditationForm({
...accreditationForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
</div>
<Switch
checked={accreditationForm.isActive}
onCheckedChange={(value) =>
setAccreditationForm({
...accreditationForm,
isActive: value,
})
}
/>
</div>
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingAccreditation ? 'Save Changes' : 'Create Item'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,263 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingBanner: any;
bannerForm: any;
setBannerForm: any;
onSave: () => void;
}
export default function HomepageBannerModal({
open,
onOpenChange,
editingBanner,
bannerForm,
setBannerForm,
onSave,
}: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-4xl h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingBanner ? 'Edit Homepage Banner' : 'Create Homepage Banner'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 space-y-8">
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Media Configuration</h3>
<p className="text-sm text-muted-foreground">Manage your banner files for desktop and mobile layouts</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="font-semibold">Media Type</Label>
<Select
value={bannerForm.mediaType}
onValueChange={(v) =>
setBannerForm({
...bannerForm,
mediaType: v,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select media type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMAGE">Image Asset</SelectItem>
<SelectItem value="VIDEO">Video Loop</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
<p className="text-sm text-muted-foreground">Publish this banner live on the homepage</p>
</div>
<Switch
checked={bannerForm.isActive}
onCheckedChange={(val) =>
setBannerForm({
...bannerForm,
isActive: val,
})
}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="font-semibold">Desktop Media URL</Label>
<p className="text-xs text-muted-foreground">
{bannerForm.mediaType === 'VIDEO'
? 'Recommended: 1920 × 650 MP4 Format '
: 'Recommended: 1920 × 650 Widescreen'}
</p>
<BytescaleUploader
value={bannerForm.desktopMediaUrl || ''}
folderPath="/homepage-banners"
onChange={(url) =>
setBannerForm({
...bannerForm,
desktopMediaUrl: url,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Mobile Media URL (Optional)</Label>
<p className="text-xs text-muted-foreground">
{bannerForm.mediaType === 'VIDEO' ? 'Recommended: 340 × 390 MP4 Format' : 'Recommended: 340 × 390 '}
</p>
<BytescaleUploader
value={bannerForm.mobileMediaUrl || ''}
folderPath="/homepage-banners"
onChange={(url) =>
setBannerForm({
...bannerForm,
mobileMediaUrl: url,
})
}
/>
</div>
</div>
</div>
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Banner Copy & Styles</h3>
<p className="text-sm text-muted-foreground">Modify text details and text alignment configurations</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Main Heading / Title</Label>
<Input
value={bannerForm.title || ''}
placeholder="e.g., Advanced Healthcare, Exceptional Compassion"
onChange={(e) =>
setBannerForm({
...bannerForm,
title: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Sub-Heading / Subtitle</Label>
<Input
value={bannerForm.subtitle || ''}
placeholder="e.g., Book appointments online with top multi-specialty doctors."
onChange={(e) =>
setBannerForm({
...bannerForm,
subtitle: e.target.value,
})
}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Text Alignment Alignment</Label>
<Select
value={bannerForm.textAlignment || 'left'}
onValueChange={(v) =>
setBannerForm({
...bannerForm,
textAlignment: v,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select text position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left Aligned</SelectItem>
<SelectItem value="center">Center Aligned</SelectItem>
<SelectItem value="right">Right Aligned</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="font-semibold">Display Priority Order</Label>
<Input
type="number"
value={bannerForm.sortOrder}
onChange={(e) =>
setBannerForm({
...bannerForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
</div>
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Call To Action (CTA Button)</h3>
<p className="text-sm text-muted-foreground">Hyperlinks for optional button element overlays</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Button Label</Label>
<Input
value={bannerForm.buttonText || ''}
placeholder="e.g., Find a Doctor"
onChange={(e) =>
setBannerForm({
...bannerForm,
buttonText: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Button Redirect Link</Label>
<Input
value={bannerForm.buttonLink || ''}
placeholder="e.g., /doctors or https://..."
onChange={(e) =>
setBannerForm({
...bannerForm,
buttonLink: e.target.value,
})
}
/>
</div>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Target Tab Redirection</p>
<p className="text-sm text-muted-foreground">
Force-launch the button link target into a completely new browser tab window
</p>
</div>
<Switch
checked={bannerForm.openInNewTab}
onCheckedChange={(val) =>
setBannerForm({
...bannerForm,
openInNewTab: val,
})
}
/>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingBanner ? 'Save Changes' : 'Create Banner'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,70 +1,55 @@
import { useState, useRef } from 'react'; import {useState, useRef} from "react";
import { Button } from '@/components/ui/button'; import {Button} from "@/components/ui/button";
import { User, X, Loader2, Video } from 'lucide-react'; import {User, X, Loader2} from "lucide-react";
import axios from 'axios'; import axios from "axios";
interface BytescaleUploaderProps { interface BytescaleUploaderProps {
value: string; value: string;
onChange: (url: string) => void; onChange: (url: string) => void;
folderPath: folderPath: "/doctors" | "/departments" | "/news" | "/blog";
| '/health-packages'
| '/seo'
| '/doctors'
| '/departments'
| '/news'
| '/blog'
| '/doctor-og'
| '/homepage-banners'
| '/insurance-partners'
| '/accreditations';
} }
export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) { export function BytescaleUploader({
value,
onChange,
folderPath,
}: BytescaleUploaderProps) {
const baseURL = import.meta.env.VITE_API_URL; const baseURL = import.meta.env.VITE_API_URL;
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const isVideo = (url: string) => {
return /\.(mp4|webm|ogg)$/i.test(url);
};
const onFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => { const onFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) return; if (!file) return;
const maxSize = file.type.startsWith('video/') ? 10 * 1024 * 1024 : 5 * 1024 * 1024; if (file.size > 5 * 1024 * 1024) {
alert("File is too large (Max 5MB)");
if (file.size > maxSize) {
alert(file.type.startsWith('video/') ? 'Video is too large (Max 10MB)' : 'Image is too large (Max 5MB)');
return; return;
} }
setIsUploading(true); setIsUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append("file", file);
formData.append('folderPath', folderPath); formData.append("folderPath", folderPath);
try { try {
const response = await axios.post(`${baseURL}/upload`, formData, { const response = await axios.post(`${baseURL}/upload`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', "Content-Type": "multipart/form-data",
}, },
}); });
const { fileUrl } = response.data; const {fileUrl} = response.data;
onChange(fileUrl); onChange(fileUrl);
} catch (e: any) { } catch (e: any) {
console.error('Upload Error:', e); console.error("Upload Error:", e);
const errorMessage = e.response?.data?.error || e.message || 'Upload failed'; const errorMessage =
e.response?.data?.error || e.message || "Upload failed";
alert(`Upload Error: ${errorMessage}`); alert(`Upload Error: ${errorMessage}`);
} finally { } finally {
setIsUploading(false); setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = "";
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} }
}; };
@@ -74,19 +59,14 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
<div className="relative"> <div className="relative">
{value ? ( {value ? (
<> <>
{isVideo(value) ? ( <img
<video src={value} className="w-20 h-20 rounded-md object-cover border-2 border-primary/20" controls /> src={value}
) : ( className="w-16 h-16 rounded-full object-cover border-2 border-primary/20"
<img alt="Preview"
src={value} />
className="w-16 h-16 rounded-full object-cover border-2 border-primary/20"
alt="Preview"
/>
)}
<button <button
type="button" type="button"
onClick={() => onChange('')} onClick={() => onChange("")}
className="absolute -top-1 -right-1 bg-destructive text-white rounded-full p-0.5 shadow-sm hover:scale-110 transition-transform" className="absolute -top-1 -right-1 bg-destructive text-white rounded-full p-0.5 shadow-sm hover:scale-110 transition-transform"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@@ -107,7 +87,7 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
onChange={onFileSelected} onChange={onFileSelected}
accept="image/jpeg,image/png,image/webp,video/mp4,video/webm,video/ogg" accept="image/jpeg,image/png,image/webp"
className="hidden" className="hidden"
/> />
@@ -124,16 +104,17 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
Uploading... Uploading...
</> </>
) : value ? ( ) : value ? (
'Change File' "Change Photo"
) : ( ) : (
'Upload Image / Video' "Upload Photo"
)} )}
</Button> </Button>
</div> </div>
{value && ( {value && (
<p className="text-xs text-amber-600 pl-[72px]"> <p className="text-xs text-amber-600 pl-[72px]">
Make sure to save the changes by clicking the "Save Changes" button. Make sure to save the changes by clicking the "Save Changes"
button.
</p> </p>
)} )}
</div> </div>
@@ -1,397 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import SeoFields from '@/components/SeoFields/SeoFields';
import { useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Plus, Trash2 } from 'lucide-react';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingPackage: any;
pkgForm: any;
setPkgForm: any;
inclusionsList: any[];
setInclusionsList: any;
categories: any[];
onSave: () => void;
}
export default function HealthPackageModal({
open,
onOpenChange,
editingPackage,
pkgForm,
setPkgForm,
inclusionsList,
setInclusionsList,
categories,
onSave,
}: Props) {
useEffect(() => {
if (!editingPackage && pkgForm.name) {
setPkgForm((prev: any) => ({
...prev,
slug: prev.slug
? prev.slug
: pkgForm.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, ''),
}));
}
}, [pkgForm.name]);
const handleAddInclusionField = () => {
setInclusionsList([
...inclusionsList,
{
id: Date.now(),
category: '',
items: '',
},
]);
};
const handleRemoveInclusionField = (id: number) => {
setInclusionsList(inclusionsList.filter((item) => item.id !== id));
};
const handleUpdateInclusionField = (id: number, field: string, value: string) => {
setInclusionsList(
inclusionsList.map((item) =>
item.id === id
? {
...item,
[field]: value,
}
: item
)
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-7xl h-[92vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingPackage ? 'Edit Health Package' : 'Create Health Package'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 xl:grid-cols-[1.2fr_0.8fr] gap-8 p-6">
{/* LEFT COLUMN */}
<div className="space-y-8">
<div className="space-y-5">
<div className="sticky top-0 bg-background z-10 pb-2">
<h3 className="text-lg font-bold">Profile & Pricing</h3>
<p className="text-sm text-muted-foreground">Main package information</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
<Label className="font-semibold">Package Image</Label>
<p className="text-xs text-muted-foreground">Recommended size: 650 × 250</p>
<BytescaleUploader
value={pkgForm.image || ''}
folderPath="/health-packages"
onChange={(url) =>
setPkgForm({
...pkgForm,
image: url,
})
}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active</p>
<p className="text-sm text-muted-foreground">Show publicly</p>
</div>
<Switch
checked={pkgForm.isActive}
onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isActive: val,
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Featured</p>
<p className="text-sm text-muted-foreground">Show on homepage</p>
</div>
<Switch
checked={pkgForm.isFeatured || false}
onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isFeatured: val,
})
}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Package Name</Label>
<Input
value={pkgForm.name}
onChange={(e) =>
setPkgForm({
...pkgForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">URL Slug</Label>
<Input
value={pkgForm.slug}
onChange={(e) =>
setPkgForm({
...pkgForm,
slug: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Category</Label>
<Select
value={pkgForm.categoryId?.toString()}
onValueChange={(v) =>
setPkgForm({
...pkgForm,
categoryId: Number(v),
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((c) => (
<SelectItem key={c.id} value={c.id.toString()}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Regular Price ()</Label>
<Input
type="number"
value={pkgForm.price || ''}
onChange={(e) => {
const value = e.target.value ? Number(e.target.value) : undefined;
setPkgForm({
...pkgForm,
price: value,
});
}}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Discounted Price ()</Label>
<Input
type="number"
disabled={!pkgForm.price}
value={pkgForm.discountedPrice || ''}
onChange={(e) =>
setPkgForm({
...pkgForm,
discountedPrice: e.target.value ? Number(e.target.value) : undefined,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Sort Priority</Label>
<Input
type="number"
value={pkgForm.sortOrder}
onChange={(e) =>
setPkgForm({
...pkgForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Description</Label>
<Textarea
rows={5}
value={pkgForm.description}
onChange={(e) =>
setPkgForm({
...pkgForm,
description: e.target.value,
})
}
/>
</div>
</div>
</div>
{/* INCLUSIONS */}
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">Tests & Inclusions</h3>
<p className="text-sm text-muted-foreground">Group tests into categories</p>
</div>
<Badge variant="outline">{inclusionsList.length} Groups</Badge>
</div>
<Accordion type="multiple" className="space-y-4">
{inclusionsList.map((inc, index) => {
const testCount = inc.items?.split(',').filter(Boolean).length;
return (
<AccordionItem
key={inc.id}
value={inc.id.toString()}
className="border rounded-xl bg-background px-5 shadow-sm"
>
<AccordionTrigger className="hover:no-underline w-full">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col items-start text-left">
<p className="font-semibold">{inc.category || `Group ${index + 1}`}</p>
<p className="text-xs text-muted-foreground">{testCount || 0} tests included</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-600"
onClick={() => handleRemoveInclusionField(inc.id)}
>
<Trash2 className="h-4 w-4 mr-1" />
Remove
</Button>
</div>
</AccordionTrigger>
<AccordionContent className="pt-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Category Title</Label>
<Input
placeholder="Routine Blood Tests"
value={inc.category}
onChange={(e) => handleUpdateInclusionField(inc.id, 'category', e.target.value)}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Included Tests</Label>
</div>
<Textarea
rows={4}
placeholder="CBC, LFT, RFT, TSH"
value={inc.items}
onChange={(e) => handleUpdateInclusionField(inc.id, 'items', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Separate each test using commas</p>
</div>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
<Button
variant="outline"
className="w-full border-dashed border-2 h-12"
onClick={handleAddInclusionField}
>
<Plus className="h-4 w-4 mr-2" />
Add New Inclusion Group
</Button>
</div>
</div>
{/* RIGHT COLUMN */}
<div className="space-y-6">
<SeoFields
value={pkgForm.seo}
slug={pkgForm.slug}
folderPath="/seo"
onChange={(seo) =>
setPkgForm({
...pkgForm,
seo,
})
}
/>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingPackage ? 'Save Changes' : 'Create Package'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,132 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingPartner: any;
partnerForm: any;
setPartnerForm: any;
onSave: () => void;
}
export default function InsurancePartnerModal({
open,
onOpenChange,
editingPartner,
partnerForm,
setPartnerForm,
onSave,
}: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-2xl h-auto max-h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingPartner ? 'Edit Insurance Partner' : 'Create Insurance Partner'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Company Information</h3>
<p className="text-sm text-muted-foreground">Setup profile configurations for target insurance brand</p>
</div>
<div className="space-y-2">
<Label className="font-semibold">Company / Provider Name</Label>
<Input
value={partnerForm.name || ''}
placeholder="e.g., National Insurance, Star Health Care"
onChange={(e) =>
setPartnerForm({
...partnerForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Claim Portal Redirect Link (Optional)</Label>
<Input
value={partnerForm.websiteUrl || ''}
placeholder="e.g., https://corporate-claims-portal.com"
onChange={(e) =>
setPartnerForm({
...partnerForm,
websiteUrl: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="font-semibold">Brand Logo Image</Label>
<p className="text-xs text-muted-foreground">
Recommended: Clear layout, transparent background (PNG or SVG preferred)
</p>
<BytescaleUploader
value={partnerForm.logo || ''}
folderPath="/insurance-partners"
onChange={(url) =>
setPartnerForm({
...partnerForm,
logo: url,
})
}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<div className="space-y-2">
<Label className="font-semibold">Grid Layout Sorting Rank</Label>
<Input
type="number"
value={partnerForm.sortOrder}
onChange={(e) =>
setPartnerForm({
...partnerForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
</div>
<Switch
checked={partnerForm.isActive}
onCheckedChange={(val) =>
setPartnerForm({
...partnerForm,
isActive: val,
})
}
/>
</div>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingPartner ? 'Save Changes' : 'Add Partner'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,219 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { getAllInquiriesApi, HealthInquiry } from '@/api/healthCheck';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Loader2, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react';
export default function PackageInquiriesTab() {
const [inquiries, setInquiries] = useState<HealthInquiry[]>([]);
const [loading, setLoading] = useState(true);
const [filterDate, setFilterDate] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const fetchInquiries = useCallback(async () => {
setLoading(true);
try {
const res = await getAllInquiriesApi(currentPage, itemsPerPage, filterDate, startDate, endDate);
setInquiries(res.data || []);
setTotalItems(res.pagination?.total || 0);
setTotalPages(res.pagination?.totalPages || 1);
} catch (err) {
console.error('Failed to fetch inquiries', err);
} finally {
setLoading(false);
}
}, [currentPage, itemsPerPage, filterDate, startDate, endDate]);
useEffect(() => {
fetchInquiries();
}, [fetchInquiries]);
const handleFilterChange = (setter: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setter(value);
setCurrentPage(1);
};
const indexOfFirstItem = (currentPage - 1) * itemsPerPage;
const indexOfLastItem = Math.min(currentPage * itemsPerPage, totalItems);
return (
<Card>
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<CardTitle className="text-xl">Package Inquiries</CardTitle>
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Specific Date</label>
<Input
type="date"
value={filterDate}
onChange={(e) => handleFilterChange(setFilterDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!startDate || !!endDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">From</label>
<Input
type="date"
value={startDate}
onChange={(e) => handleFilterChange(setStartDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">To</label>
<Input
type="date"
value={endDate}
onChange={(e) => handleFilterChange(setEndDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Rows</label>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm focus:ring-2 focus:ring-primary"
>
<option value={5}>5 / page</option>
<option value={10}>10 / page</option>
<option value={20}>20 / page</option>
<option value={50}>50 / page</option>
</select>
</div>
<Button variant="outline" onClick={fetchInquiries} disabled={loading}>
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</CardHeader>
<CardContent className="p-0 sm:p-6 sm:pt-0">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1000px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[150px] font-bold bg-background">Requested Date</TableHead>
<TableHead className="w-[220px] font-bold bg-background">Patient Details</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Requested Package</TableHead>
<TableHead className="w-[120px] font-bold bg-background">Age/Gender</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : inquiries.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-10">
No inquiries found for the selected criteria
</TableCell>
</TableRow>
) : (
inquiries.map((inq) => (
<TableRow key={inq.id} className="hover:bg-muted/50">
<TableCell>
<div className="font-semibold text-primary">
{new Date(inq.preferredDate).toLocaleDateString()}
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Submitted: {new Date(inq.createdAt).toLocaleDateString()}
</div>
</TableCell>
<TableCell>
<div className="font-semibold text-base">{inq.fullName}</div>
<div className="text-sm">{inq.mobileNumber}</div>
<div className="text-xs text-muted-foreground">{inq.email || '-'}</div>
</TableCell>
<TableCell>
<div className="font-semibold text-sm truncate">{inq.healthPackage?.name || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium">
{inq.age} yrs / {inq.gender}
</div>
</TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="text-sm italic line-clamp-3 text-muted-foreground whitespace-pre-wrap cursor-pointer">
{inq.message || 'No message provided.'}
</div>
</TooltipTrigger>
<TooltipContent className="max-w-md whitespace-pre-wrap">
{inq.message || 'No message provided.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loading && totalItems > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between px-2 py-4 border-t gap-4 mt-2">
<div className="text-sm text-muted-foreground">
Showing <span className="font-semibold">{indexOfFirstItem + 1}</span> to{' '}
<span className="font-semibold">{indexOfLastItem}</span> of{' '}
<span className="font-semibold">{totalItems}</span> inquiries
</div>
<div className="flex items-center gap-6">
<div className="text-sm font-semibold">
Page {currentPage} of {totalPages || 1}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}
@@ -1,8 +1,8 @@
import { Navigate } from 'react-router-dom'; import {Navigate} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute({ children }: any) { export default function ProtectedRoute({children}: any) {
const { token } = useAuth(); const {token} = useAuth();
if (!token) { if (!token) {
return <Navigate to="/" />; return <Navigate to="/" />;
@@ -1,186 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { X } from 'lucide-react';
interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
interface SeoFieldsProps {
value?: SeoData;
onChange: (seo: SeoData) => void;
slug?: string;
folderPath?: '/seo';
}
export default function SeoFields({ value, onChange, slug, folderPath = '/seo' }: SeoFieldsProps) {
const seo = value || {};
const updateSeo = (field: keyof SeoData, fieldValue: any) => {
onChange({
...seo,
[field]: fieldValue,
});
};
const removeTag = (index: number) => {
updateSeo(
'tags',
(seo.tags || []).filter((_, i) => i !== index)
);
};
return (
<div className="space-y-5 p-5 border rounded-xl bg-muted/20">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">SEO Settings</h3>
<p className="text-sm text-muted-foreground">Optimize for Google & social sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">SEO Title</Label>
<span className="text-xs text-muted-foreground">{seo.seoTitle?.length || 0}/60</span>
</div>
<Input
placeholder="Best Health Checkup Package in Kochi"
value={seo.seoTitle || ''}
onChange={(e) => updateSeo('seoTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 5060 characters</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">Meta Description</Label>
<span className="text-xs text-muted-foreground">{seo.metaDescription?.length || 0}/160</span>
</div>
<Textarea
rows={4}
placeholder="Short description shown in Google search results"
value={seo.metaDescription || ''}
onChange={(e) => updateSeo('metaDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 150160 characters</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Focus Keyphrase</Label>
<Input
placeholder="health checkup package kochi"
value={seo.focusKeyphrase || ''}
onChange={(e) => updateSeo('focusKeyphrase', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Tags / Keywords</Label>
<div className="flex flex-wrap gap-2 border rounded-md p-3 min-h-[48px] bg-background">
{(seo.tags || []).map((tag, index) => (
<div
key={index}
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
>
<span>{tag}</span>
<button type="button" onClick={() => removeTag(index)} className="hover:text-red-500 transition-colors">
<X className="h-3 w-3" />
</button>
</div>
))}
<Input
placeholder="Type keyword and press Enter"
className="border-0 shadow-none focus-visible:ring-0 min-w-[220px] flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!(seo.tags || []).includes(newTag)) {
updateSeo('tags', [...(seo.tags || []), newTag]);
}
e.currentTarget.value = '';
}
}}
/>
</div>
<p className="text-xs text-muted-foreground">Press Enter to add tags</p>
</div>
<div className="border-t pt-5 space-y-5">
<div className="flex items-center justify-between">
<div>
<h4 className="font-bold">Open Graph (Social Preview)</h4>
<p className="text-sm text-muted-foreground">Facebook, WhatsApp & Twitter sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Title</Label>
<Input
placeholder="Title for social sharing"
value={seo.ogTitle || ''}
onChange={(e) => updateSeo('ogTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, SEO title will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Description</Label>
<Textarea
rows={4}
placeholder="Description for social sharing"
value={seo.ogDescription || ''}
onChange={(e) => updateSeo('ogDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, meta description will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Image</Label>
<BytescaleUploader
value={seo.ogImage || ''}
folderPath={folderPath}
onChange={(url) => updateSeo('ogImage', url)}
/>
</div>
</div>
</div>
);
}
@@ -1,113 +0,0 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
interface SeoPreviewData {
seo?: {
ogImage?: string;
ogTitle?: string;
seoTitle?: string;
ogDescription?: string;
metaDescription?: string;
slug?: string;
};
doctorId?: string;
name?: string;
}
interface SeoPreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
previewData?: SeoPreviewData | null;
url?: string;
title?: string;
}
export default function SeoPreview({ open, onOpenChange, previewData, url, title = 'SEO Preview' }: SeoPreviewProps) {
const previewUrl = url || 'https://www.gg-hospital.com';
const hasSeoData =
!!previewData?.seo &&
!!(
previewData.seo.ogImage ||
previewData.seo.ogTitle ||
previewData.seo.seoTitle ||
previewData.seo.ogDescription ||
previewData.seo.metaDescription
);
const imageUrl = previewData?.seo?.ogImage || 'https://placehold.co/1200x630?text=GG+Hospital';
const ogTitle = previewData?.seo?.ogTitle || previewData?.seo?.seoTitle || 'GG Hospital';
const ogDescription =
previewData?.seo?.ogDescription || previewData?.seo?.metaDescription || 'No description available';
const searchTitle = previewData?.seo?.seoTitle || previewData?.seo?.ogTitle || 'SEO title preview';
const searchDescription =
previewData?.seo?.metaDescription || previewData?.seo?.ogDescription || 'No meta description available';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:!max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="text-xl">{title}</DialogTitle>
</DialogHeader>
{hasSeoData ? (
<div className="space-y-10 py-2">
{/* Social Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">
Social Media Preview (WhatsApp / Facebook)
</p>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="block max-w-[560px] overflow-hidden rounded-xl border bg-white shadow-sm transition hover:shadow-md"
>
<div className="aspect-[1.91/1] overflow-hidden bg-muted">
<img src={imageUrl} alt="OG Preview" className="h-full w-full object-cover" />
</div>
<div className="border-t bg-[#f0f2f5] px-4 py-3">
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">{ogTitle}</h3>
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">{ogDescription}</p>
<p className="mt-1 truncate text-[11px] tracking-wide text-[#65676b]">{previewUrl}</p>
</div>
</a>
</div>
{/* Google Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">Google Search Preview</p>
<div className="rounded-xl border bg-white p-6">
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="block">
<p className="truncate text-[14px] text-[#202124] hover:underline">{previewUrl}</p>
<h3 className="mt-1 text-[22px] leading-tight text-[#1a0dab] hover:underline">{searchTitle}</h3>
</a>
<p className="mt-2 line-clamp-3 text-[14px] leading-6 text-[#4d5156]">{searchDescription}</p>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center py-16 text-sm text-muted-foreground">
No preview data available.
</div>
)}
<DialogFooter className="mt-0 border-t bg-background p-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+8 -8
View File
@@ -1,16 +1,16 @@
import { useState, useEffect } from 'react'; import {useState, useEffect} from "react";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
import { Button } from '@/components/ui/button'; import {Button} from "@/components/ui/button";
import { Switch } from '@/components/ui/switch'; import {Switch} from "@/components/ui/switch";
import { log } from 'console'; import {log} from "console";
export default function Header() { export default function Header() {
const { user, logout } = useAuth(); const {user, logout} = useAuth();
const [darkMode, setDarkMode] = useState<boolean>(false); const [darkMode, setDarkMode] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (darkMode) document.documentElement.classList.add('dark'); if (darkMode) document.documentElement.classList.add("dark");
else document.documentElement.classList.remove('dark'); else document.documentElement.classList.remove("dark");
}, [darkMode]); }, [darkMode]);
return ( return (
+26 -73
View File
@@ -1,71 +1,51 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from "react-router-dom";
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button";
import { Separator } from '@/components/ui/separator'; import { Separator } from "@/components/ui/separator";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown } from 'lucide-react';
export default function Sidebar() { export default function Sidebar() {
const location = useLocation(); const location = useLocation();
const navItems = [ const navItems = [
{ {
name: 'Department', name: "Department",
path: '/department', path: "/department",
}, },
{ {
name: 'Doctor', name: "Doctor",
path: '/doctor', path: "/doctor",
}, },
{ {
name: 'Health Check', name: "Appointments",
path: '/health-check', path: "/appointment",
}, },
{ {
name: 'Appointments', name: "Career",
path: '/appointment', path: "/career",
}, },
{ {
name: 'Career', name: "Candidates",
path: '/career', path: "/candidate",
}, },
{ {
name: 'Candidates', name: "Inquiry",
path: '/candidate', path: "/inquiry",
}, },
{ {
name: 'Inquiry', name: "Academics & Research",
path: '/inquiry', path: "/academics",
}, },
{ {
name: 'Academics & Research', name: "News & Media",
path: '/academics', path: "/news",
}, },
{ {
name: 'News & Media', name: "Email",
path: '/news', path: "/email",
}, },
{ {
name: 'Email', name: "Blog",
path: '/email', path: "/blog",
},
{
name: 'Blog',
path: '/blog',
},
{
name: 'Homepage Content',
children: [
{
name: 'Homepage Banner',
path: '/homepage-banner',
},
{
name: 'Insurance Partner',
path: '/insurance-partner',
},
{ name: 'Accreditation', path: '/accreditation' },
],
}, },
]; ];
@@ -79,40 +59,13 @@ export default function Sidebar() {
<nav className="p-4 space-y-2"> <nav className="p-4 space-y-2">
{navItems.map((item) => { {navItems.map((item) => {
if ('children' in item) {
const hasActiveChild = item.children.some((child) => location.pathname === child.path);
return (
<Collapsible key={item.name} defaultOpen={hasActiveChild}>
<CollapsibleTrigger asChild>
<Button variant={hasActiveChild ? 'secondary' : 'ghost'} className="w-full justify-between">
<span>{item.name}</span>
<ChevronDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-1 space-y-1">
{item.children.map((child) => {
const active = location.pathname === child.path;
return (
<Link key={child.path} to={child.path}>
<Button variant={active ? 'secondary' : 'ghost'} className="w-full justify-start pl-8">
{child.name}
</Button>
</Link>
);
})}
</CollapsibleContent>
</Collapsible>
);
}
const active = location.pathname === item.path; const active = location.pathname === item.path;
return ( return (
<Link key={item.path} to={item.path}> <Link key={item.path} to={item.path}>
<Button variant={active ? 'secondary' : 'ghost'} className="w-full justify-start"> <Button
variant={active ? "secondary" : "ghost"}
className="w-full justify-start">
{item.name} {item.name}
</Button> </Button>
</Link> </Link>
-61
View File
@@ -1,61 +0,0 @@
import * as React from 'react';
import { Accordion as AccordionPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
function Accordion({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />;
}
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
);
}
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
className
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
{...props}
>
<div
className={cn(
'h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
className
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

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