diff --git a/backend/prisma/migrations/20260513114221_healthcheck_api/migration.sql b/backend/prisma/migrations/20260513114221_healthcheck_api/migration.sql new file mode 100644 index 0000000..224a846 --- /dev/null +++ b/backend/prisma/migrations/20260513114221_healthcheck_api/migration.sql @@ -0,0 +1,63 @@ +-- 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; diff --git a/backend/prisma/migrations/20260514100534_add_gender_and_age_field/migration.sql b/backend/prisma/migrations/20260514100534_add_gender_and_age_field/migration.sql new file mode 100644 index 0000000..299d496 --- /dev/null +++ b/backend/prisma/migrations/20260514100534_add_gender_and_age_field/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER, +ADD COLUMN "gender" TEXT; diff --git a/backend/prisma/migrations/20260514110339_default_inclusion/migration.sql b/backend/prisma/migrations/20260514110339_default_inclusion/migration.sql new file mode 100644 index 0000000..52e227a --- /dev/null +++ b/backend/prisma/migrations/20260514110339_default_inclusion/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}'; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b9988d4..d35b4ec 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -220,3 +220,58 @@ model NewsImage { 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) + 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[] + + 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 +} diff --git a/backend/src/app.js b/backend/src/app.js index 21593f6..0bb2b1f 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -15,6 +15,7 @@ import academicsResearchRoutes from "./routes/academicsResearch.routes.js"; import emailConfigRoutes from "./routes/emailConfig.routes.js"; import newsMediaRoutes from "./routes/newsMedia.routes.js"; import importRoutes from "./routes/importRoutes.js"; +import healthCheckRoutes from "./routes/healthCheck.route.js"; dotenv.config(); @@ -55,6 +56,7 @@ app.use("/api/academics", academicsResearchRoutes); app.use("/api/email", emailConfigRoutes); app.use("/api/newsMedia", newsMediaRoutes); app.use("/api/import", importRoutes); +app.use("/api/health-check", healthCheckRoutes); const PORT = process.env.PORT || 5008; app.listen(PORT, () => { diff --git a/backend/src/controllers/healthCheck.controller.js b/backend/src/controllers/healthCheck.controller.js new file mode 100644 index 0000000..6803063 --- /dev/null +++ b/backend/src/controllers/healthCheck.controller.js @@ -0,0 +1,436 @@ +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, + 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); + + 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 }, + 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, + discountedPrice, + inclusions, + categoryId, + isActive, + isFeatured, + sortOrder, + } = req.body; + + const healthPackage = await prisma.healthPackage.create({ + data: { + name, + slug, + description, + price, + discountedPrice, + inclusions, + categoryId: Number(categoryId), + isActive: isActive ?? true, + isFeatured: isFeatured ?? false, + sortOrder: sortOrder ? Number(sortOrder) : 1000, + }, + }); + + 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; + + if (data.categoryId) data.categoryId = Number(data.categoryId); + if (data.sortOrder) data.sortOrder = Number(data.sortOrder); + + const updated = await prisma.healthPackage.update({ + where: { id: Number(id) }, + data, + }); + + 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: ` +
+ +
+ + +
+

GG Hospital

+

+ New Health Checkup Package Inquiry +

+
+ + +
+ +

Inquirer Details

+ + + + + + + + + + + + + + + + + + + + + + +
Name:${fullName}
Phone:${mobileNumber}
Email:${email || "-"}
Age:${age || "-"}
Gender:${gender || "-"}
+ +

Package Details

+ + + + + + + + + + +
Package Name:${inquiry.healthPackage?.name || "Unknown Package"}
Preferred Date: + ${ + preferredDate + ? new Date(preferredDate).toLocaleDateString("en-GB", { + day: "2-digit", + month: "long", + year: "numeric", + }) + : "Not specified" + } +
+ + +
+

Message

+
+ ${message ? message.replace(/\n/g, "
") : "-"} +
+
+ +
+ + +
+ This inquiry was submitted via the GG Hospital website. +
+ +
+ +
+ `, + }); + } + } 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 }, + }); + + 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" }); + } +}; diff --git a/backend/src/routes/healthCheck.route.js b/backend/src/routes/healthCheck.route.js new file mode 100644 index 0000000..2049dcd --- /dev/null +++ b/backend/src/routes/healthCheck.route.js @@ -0,0 +1,39 @@ +import express from "express"; +import { + // Categories + getAllCategories, + getPackageBySlug, + createCategory, + updateCategory, + deleteCategory, + + // Packages + getAllPackages, + createPackage, + updatePackage, + deletePackage, + + // 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("/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;