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/migrations/20260518051754_make_slug_optional/migration.sql b/backend/prisma/migrations/20260518051754_make_slug_optional/migration.sql
new file mode 100644
index 0000000..b41cd76
--- /dev/null
+++ b/backend/prisma/migrations/20260518051754_make_slug_optional/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
diff --git a/backend/prisma/migrations/20260518055117_health_checkup_optional_image_field/migration.sql b/backend/prisma/migrations/20260518055117_health_checkup_optional_image_field/migration.sql
new file mode 100644
index 0000000..f6b74f9
--- /dev/null
+++ b/backend/prisma/migrations/20260518055117_health_checkup_optional_image_field/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index b9988d4..f31c375 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -220,3 +220,59 @@ 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)
+ 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[]
+
+ 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..7ee86bb
--- /dev/null
+++ b/backend/src/controllers/healthCheck.controller.js
@@ -0,0 +1,440 @@
+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 },
+ 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,
+ } = 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,
+ },
+ });
+
+ 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;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 54fcdf9..bfc0b15 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -23,6 +23,7 @@ import AcademicsPage from "./pages/Academics";
import NewsPage from "./pages/newsMedia";
import BlogDetail from "./pages/BlogDetails";
import ImportData from "./pages/ImportData";
+import HealthPackagePage from "./pages/HealthPackagePage";
export default function App() {
return (
@@ -51,6 +52,7 @@ export default function App() {
} />
} />
} />
+ } />
diff --git a/frontend/src/api/healthCheck.ts b/frontend/src/api/healthCheck.ts
new file mode 100644
index 0000000..ae5430e
--- /dev/null
+++ b/frontend/src/api/healthCheck.ts
@@ -0,0 +1,160 @@
+import apiClient from "@/api/client";
+import toast from "react-hot-toast";
+
+export interface HealthPackage {
+ id?: number;
+ name: string;
+ slug: string;
+ description?: string;
+ price: number;
+ image?: string;
+ discountedPrice?: number;
+ inclusions: Record;
+ categoryId: number;
+ isActive: boolean;
+ isFeatured: boolean;
+ sortOrder: number;
+ 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) => {
+ 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,
+) => {
+ 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;
+};
diff --git a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
index e4e1646..cc412f3 100644
--- a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
+++ b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx
@@ -1,12 +1,17 @@
-import {useState, useRef} from "react";
-import {Button} from "@/components/ui/button";
-import {User, X, Loader2} from "lucide-react";
+import { useState, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { User, X, Loader2 } from "lucide-react";
import axios from "axios";
interface BytescaleUploaderProps {
value: string;
onChange: (url: string) => void;
- folderPath: "/doctors" | "/departments" | "/news" | "/blog";
+ folderPath:
+ | "/doctors"
+ | "/departments"
+ | "/news"
+ | "/blog"
+ | "/health-packages";
}
export function BytescaleUploader({
@@ -40,7 +45,7 @@ export function BytescaleUploader({
},
});
- const {fileUrl} = response.data;
+ const { fileUrl } = response.data;
onChange(fileUrl);
} catch (e: any) {
console.error("Upload Error:", e);
diff --git a/frontend/src/components/PackageInquiriesTab/PackageInquiriesTab.tsx b/frontend/src/components/PackageInquiriesTab/PackageInquiriesTab.tsx
new file mode 100644
index 0000000..0d13eff
--- /dev/null
+++ b/frontend/src/components/PackageInquiriesTab/PackageInquiriesTab.tsx
@@ -0,0 +1,273 @@
+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([]);
+ 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>,
+ value: string,
+ ) => {
+ setter(value);
+ setCurrentPage(1);
+ };
+
+ const indexOfFirstItem = (currentPage - 1) * itemsPerPage;
+ const indexOfLastItem = Math.min(currentPage * itemsPerPage, totalItems);
+
+ return (
+
+
+ Package Inquiries
+
+
+
+
+
+ handleFilterChange(setFilterDate, e.target.value)
+ }
+ className="w-[140px] text-sm"
+ disabled={!!startDate || !!endDate}
+ />
+
+
+
+
+ handleFilterChange(setStartDate, e.target.value)}
+ className="w-[140px] text-sm"
+ disabled={!!filterDate}
+ />
+
+
+
+
+ handleFilterChange(setEndDate, e.target.value)}
+ className="w-[140px] text-sm"
+ disabled={!!filterDate}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Requested Date
+
+
+ Patient Details
+
+
+ Requested Package
+
+
+ Age/Gender
+
+
+ Message
+
+
+
+
+ {loading ? (
+
+
+
+
+
+ ) : inquiries.length === 0 ? (
+
+
+ No inquiries found for the selected criteria
+
+
+ ) : (
+ inquiries.map((inq) => (
+
+
+
+ {new Date(inq.preferredDate).toLocaleDateString()}
+
+
+ Submitted:{" "}
+ {new Date(inq.createdAt).toLocaleDateString()}
+
+
+
+
+ {inq.fullName}
+
+ {inq.mobileNumber}
+
+ {inq.email || "-"}
+
+
+
+
+ {inq.healthPackage?.name || "N/A"}
+
+
+
+
+ {inq.age} yrs / {inq.gender}
+
+
+
+
+
+
+ {inq.message || "No message provided."}
+
+
+
+
+ {inq.message || "No message provided."}
+
+
+
+
+ ))
+ )}
+
+
+
+
+ {!loading && totalItems > 0 && (
+
+
+ Showing{" "}
+ {indexOfFirstItem + 1} to{" "}
+ {indexOfLastItem} of{" "}
+ {totalItems} inquiries
+
+
+
+ Page {currentPage} of {totalPages || 1}
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 2610239..b59650a 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -15,6 +15,10 @@ export default function Sidebar() {
name: "Doctor",
path: "/doctor",
},
+ {
+ name: "Health Check",
+ path: "/health-check",
+ },
{
name: "Appointments",
path: "/appointment",
@@ -65,7 +69,8 @@ export default function Sidebar() {
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index c88ffd6..6138844 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
@@ -25,7 +25,7 @@ const buttonVariants = cva(
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx
index f4cbece..c44b1db 100644
--- a/frontend/src/components/ui/dialog.tsx
+++ b/frontend/src/components/ui/dialog.tsx
@@ -59,7 +59,7 @@ function DialogContent({
)
diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx
new file mode 100644
index 0000000..8333850
--- /dev/null
+++ b/frontend/src/components/ui/select.tsx
@@ -0,0 +1,190 @@
+import * as React from "react"
+import { Select as SelectPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
+
+function Select({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..72465b2
--- /dev/null
+++ b/frontend/src/components/ui/tabs.tsx
@@ -0,0 +1,88 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Tabs as TabsPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const tabsListVariants = cva(
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..7413f6e
--- /dev/null
+++ b/frontend/src/components/ui/tooltip.tsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { Tooltip as TooltipPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
diff --git a/frontend/src/pages/HealthPackagePage.tsx b/frontend/src/pages/HealthPackagePage.tsx
new file mode 100644
index 0000000..02b793b
--- /dev/null
+++ b/frontend/src/pages/HealthPackagePage.tsx
@@ -0,0 +1,1075 @@
+import { useState, useEffect, useCallback, useMemo } from "react";
+import { AxiosError } from "axios";
+import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
+
+import {
+ getHealthPackagesApi,
+ getHealthCategoriesApi,
+ createHealthPackageApi,
+ updateHealthPackageApi,
+ createCategoryApi,
+ updateCategoryApi,
+ deleteCategoryApi,
+ HealthPackage,
+ HealthCategory,
+} from "@/api/healthCheck";
+
+import PackageInquiriesTab from "@/components/PackageInquiriesTab/PackageInquiriesTab";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+import {
+ Loader2,
+ RefreshCw,
+ Plus,
+ Pencil,
+ ChevronLeft,
+ ChevronRight,
+ LayoutGrid,
+ Eye,
+ Trash2,
+} from "lucide-react";
+
+export default function HealthPackagePage() {
+ const [packages, setPackages] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ // Modals
+ const [packageModal, setPackageModal] = useState(false);
+ const [categoryModal, setCategoryModal] = useState(false);
+ const [viewModal, setViewModal] = useState(false);
+
+ // States
+ const [selectedPackage, setSelectedPackage] = useState(
+ null,
+ );
+ const [editingPackage, setEditingPackage] = useState(
+ null,
+ );
+ const [editingCategory, setEditingCategory] = useState(
+ null,
+ );
+
+ // Filters & Pagination
+ const [searchText, setSearchText] = useState("");
+ const [filterCategory, setFilterCategory] = useState("");
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // Forms
+ const [pkgForm, setPkgForm] = useState>({
+ name: "",
+ slug: "",
+ description: "",
+ image: "",
+ price: 0,
+ discountedPrice: 0,
+ categoryId: 0,
+ isActive: true,
+ sortOrder: 1000,
+ });
+ const [inclusionsList, setInclusionsList] = useState([
+ { id: Date.now(), category: "", items: "" },
+ ]);
+ const [catForm, setCatForm] = useState({
+ name: "",
+ slug: "",
+ sortOrder: 1000,
+ isActive: true,
+ });
+
+ const fetchData = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ try {
+ const [p, c] = await Promise.all([
+ getHealthPackagesApi(),
+ getHealthCategoriesApi(),
+ ]);
+ setPackages(p.data || []);
+ setCategories(c.data || []);
+ } catch (err) {
+ if (err instanceof AxiosError) {
+ setError(err.response?.data?.message || "Failed to load data");
+ } else {
+ setError("Something went wrong");
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ // --- Package Filtering & Pagination ---
+ const filteredPackages = useMemo(() => {
+ return packages.filter((pkg) => {
+ const matchesSearch =
+ pkg.name.toLowerCase().includes(searchText.toLowerCase()) ||
+ pkg.category?.name.toLowerCase().includes(searchText.toLowerCase());
+ const matchesCat = filterCategory
+ ? pkg.categoryId === Number(filterCategory)
+ : true;
+ return matchesSearch && matchesCat;
+ });
+ }, [packages, searchText, filterCategory]);
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [searchText, filterCategory]);
+
+ const totalPages = Math.ceil(filteredPackages.length / itemsPerPage);
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = filteredPackages.slice(
+ indexOfFirstItem,
+ indexOfLastItem,
+ );
+
+ // --- Actions ---
+ const handleToggleStatus = async (pkg: HealthPackage) => {
+ if (!pkg.id) return;
+ try {
+ await updateHealthPackageApi(pkg.id, { isActive: !pkg.isActive });
+ fetchData();
+ } catch (err) {
+ console.error("Failed to update status", err);
+ }
+ };
+ const handleToggleCategoryStatus = async (cat: HealthCategory) => {
+ if (!cat.id) return;
+ try {
+ if (cat.isActive) {
+ const proceed = window.confirm(
+ "Hiding this category will also hide all packages inside it. Proceed?",
+ );
+ if (!proceed) return;
+ }
+
+ await updateCategoryApi(cat.id, { isActive: !cat.isActive });
+ fetchData();
+ } catch (err) {
+ console.error("Failed to update category status", err);
+ }
+ };
+
+ const openAddPackage = () => {
+ setEditingPackage(null);
+ setPkgForm({
+ name: "",
+ slug: "",
+ description: "",
+ image: "",
+ price: 0,
+ discountedPrice: 0,
+ categoryId: categories[0]?.id || 0,
+ isActive: true,
+ sortOrder: 1000,
+ });
+ setInclusionsList([{ id: Date.now(), category: "", items: "" }]);
+ setPackageModal(true);
+ };
+
+ const openEditPackage = (pkg: any) => {
+ setEditingPackage(pkg);
+ setPkgForm(pkg);
+
+ if (
+ pkg.inclusions &&
+ typeof pkg.inclusions === "object" &&
+ !Array.isArray(pkg.inclusions)
+ ) {
+ const formattedList = Object.entries(pkg.inclusions).map(
+ ([cat, items], idx) => ({
+ id: Date.now() + idx,
+ category: cat,
+ items: (items as string[]).join(", "),
+ }),
+ );
+ setInclusionsList(
+ formattedList.length
+ ? formattedList
+ : [{ id: Date.now(), category: "", items: "" }],
+ );
+ } else {
+ setInclusionsList([{ id: Date.now(), category: "", items: "" }]);
+ }
+
+ setPackageModal(true);
+ };
+ 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,
+ ),
+ );
+ };
+
+ const savePackage = async () => {
+ try {
+ // Convert the dynamic array back into the required JSON object format
+ const parsedInclusions: Record = {};
+ inclusionsList.forEach((entry) => {
+ const catName = entry.category.trim();
+ if (catName) {
+ parsedInclusions[catName] = entry.items
+ .split(",")
+ .map((i) => i.trim())
+ .filter(Boolean);
+ }
+ });
+
+ const finalData = { ...pkgForm, inclusions: parsedInclusions };
+
+ if (editingPackage?.id) {
+ const changedFields: Record = {};
+ Object.keys(finalData).forEach((key) => {
+ const k = key as keyof HealthPackage;
+ if (
+ JSON.stringify(finalData[k]) !== JSON.stringify(editingPackage[k])
+ ) {
+ changedFields[k] = finalData[k];
+ }
+ });
+
+ delete changedFields.id;
+ delete changedFields.category;
+
+ if (Object.keys(changedFields).length === 0) {
+ setPackageModal(false);
+ return;
+ }
+
+ await updateHealthPackageApi(editingPackage.id, changedFields);
+ } else {
+ await createHealthPackageApi(finalData);
+ }
+
+ setPackageModal(false);
+ fetchData();
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ const saveCategory = async () => {
+ try {
+ if (editingCategory?.id) {
+ const changedFields: Record = {};
+
+ Object.keys(catForm).forEach((key) => {
+ const k = key as keyof HealthCategory;
+ if (catForm[k] !== editingCategory[k]) {
+ changedFields[k] = catForm[k];
+ }
+ });
+
+ delete changedFields.id;
+ delete changedFields._count;
+
+ if (Object.keys(changedFields).length === 0) {
+ setCategoryModal(false);
+ return;
+ }
+
+ await updateCategoryApi(
+ editingCategory.id,
+ changedFields as Partial,
+ );
+ } else {
+ await createCategoryApi(catForm as any);
+ }
+
+ setCategoryModal(false);
+ fetchData();
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ const deleteCategory = async (id: number) => {
+ if (confirm("Delete this category? Ensure no packages are linked to it.")) {
+ await deleteCategoryApi(id);
+ fetchData();
+ }
+ };
+
+ return (
+
+
+
Health Packages
+
+
+
setSearchText(e.target.value)}
+ className="w-[250px] text-base"
+ />
+
+
+
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ Packages
+ Categories
+ Inquiries
+
+
+ {/* PACKAGES TAB */}
+
+
+
+ Package List
+
+
+
+
+
+
+
+ Priority
+
+
+ Package Details
+
+
+ Category
+
+
+ Pricing
+
+
+ Status
+
+
+ Actions
+
+
+
+
+ {loading ? (
+
+
+
+
+
+ ) : currentItems.length === 0 ? (
+
+
+ No packages found
+
+
+ ) : (
+ currentItems.map((pkg) => (
+
+
+ {pkg.sortOrder}
+
+
+
+ {pkg.name}
+
+
+ /{pkg.slug}
+
+
+
+
+ {pkg.category?.name}
+
+
+
+
+ ₹{pkg.discountedPrice || pkg.price}
+
+ {pkg.discountedPrice &&
+ pkg.discountedPrice < pkg.price && (
+
+ ₹{pkg.price}
+
+ )}
+
+
+
+ handleToggleStatus(pkg)}
+ />
+
+ {pkg.isActive ? "Active" : "Hidden"}
+
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ {!loading && filteredPackages.length > 0 && (
+
+
+ Showing{" "}
+
+ {indexOfFirstItem + 1}
+ {" "}
+ to{" "}
+
+ {Math.min(indexOfLastItem, filteredPackages.length)}
+ {" "}
+ of{" "}
+
+ {filteredPackages.length}
+ {" "}
+ packages
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+ {/* CATEGORIES TAB */}
+
+
+
+ Category List
+
+
+
+
+
+
+
+
+ Priority
+
+
+ Category Name
+
+
+ Status
+
+
+ Actions
+
+
+
+
+ {categories.map((cat) => (
+
+
+ {cat.sortOrder}
+
+
+ {cat.name}
+
+
+
+
+
+ handleToggleCategoryStatus(cat)
+ }
+ />
+
+ {cat.isActive ? "Active" : "Hidden"}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {/* --- PACKAGE MODAL --- */}
+
+
+ {/* --- CATEGORY MODAL --- */}
+
+
+ {/* --- VIEW MODAL --- */}
+
+
+ );
+}
diff --git a/frontend/src/pages/email.tsx b/frontend/src/pages/email.tsx
index c6c97eb..2f2dcdc 100644
--- a/frontend/src/pages/email.tsx
+++ b/frontend/src/pages/email.tsx
@@ -233,6 +233,7 @@ export default function EmailPage() {
+