feat: health checkup CRUD apis

This commit is contained in:
Kailasdevdas
2026-05-15 17:46:52 +05:30
parent 3140d72e28
commit 9bc0bf406a
7 changed files with 600 additions and 0 deletions
@@ -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;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
+55
View File
@@ -220,3 +220,58 @@ 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)
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
}
+2
View File
@@ -15,6 +15,7 @@ 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";
dotenv.config(); dotenv.config();
@@ -55,6 +56,7 @@ 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);
const PORT = process.env.PORT || 5008; const PORT = process.env.PORT || 5008;
app.listen(PORT, () => { app.listen(PORT, () => {
@@ -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: `
<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 },
});
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" });
}
};
+39
View File
@@ -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;