From 8d60afdc49795e0da7665625f6d99359df6d0ad0 Mon Sep 17 00:00:00 2001 From: Kailasdevdas Date: Fri, 15 May 2026 17:58:25 +0530 Subject: [PATCH] feat: health checkup page --- frontend/src/App.tsx | 2 + frontend/src/api/healthCheck.ts | 132 +++ .../PackageInquiriesTab.tsx | 273 +++++ frontend/src/components/layout/Sidebar.tsx | 7 +- frontend/src/components/ui/button.tsx | 4 +- frontend/src/components/ui/dialog.tsx | 7 +- frontend/src/components/ui/select.tsx | 190 +++ frontend/src/components/ui/tabs.tsx | 88 ++ frontend/src/components/ui/tooltip.tsx | 55 + frontend/src/pages/HealthPackagePage.tsx | 1041 +++++++++++++++++ frontend/src/pages/email.tsx | 1 + 11 files changed, 1795 insertions(+), 5 deletions(-) create mode 100644 frontend/src/api/healthCheck.ts create mode 100644 frontend/src/components/PackageInquiriesTab/PackageInquiriesTab.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/components/ui/tooltip.tsx create mode 100644 frontend/src/pages/HealthPackagePage.tsx 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..698e26b --- /dev/null +++ b/frontend/src/api/healthCheck.ts @@ -0,0 +1,132 @@ +import apiClient from "@/api/client"; +import toast from "react-hot-toast"; + +export interface HealthPackage { + id?: number; + name: string; + slug: string; + description?: string; + price: number; + 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; +}) => { + const res = await apiClient.post("/health-check/categories", data); + return res.data; +}; + +export const updateCategoryApi = async (id: number, data: any) => { + const res = await apiClient.patch(`/health-check/categories/${id}`, data); + return res.data; +}; + +export const deleteCategoryApi = async (id: number) => { + const res = await apiClient.delete(`/health-check/categories/${id}`); + return res.data; +}; + +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/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..d436269 --- /dev/null +++ b/frontend/src/pages/HealthPackagePage.tsx @@ -0,0 +1,1041 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { AxiosError } from "axios"; + +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: "", + 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: "", + 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 --- */} + + + + + {editingPackage ? "Edit Package" : "Add Package"} + + + +
+
+
+

+ Profile & Pricing +

+
+
+ + + setPkgForm({ ...pkgForm, isActive: val }) + } + /> +
+ +
+ + + setPkgForm({ + ...pkgForm, + sortOrder: Number(e.target.value), + }) + } + className="text-base" + /> +
+ +
+ + + setPkgForm({ ...pkgForm, name: e.target.value }) + } + className="text-base" + /> +
+ +
+ + + setPkgForm({ ...pkgForm, slug: e.target.value }) + } + className="text-base" + /> +
+ +
+ + +
+ +
+
+ + + setPkgForm({ + ...pkgForm, + price: Number(e.target.value), + }) + } + className="text-base" + /> +
+
+ + + setPkgForm({ + ...pkgForm, + discountedPrice: Number(e.target.value), + }) + } + className="text-base" + /> +
+
+
+
+ +
+

+ Details & Inclusions +

+
+
+ +