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: ` +