Files
gg-backend/frontend/src/pages/HealthPackagePage.tsx
T

1114 lines
33 KiB
TypeScript
Raw Normal View History

2026-05-15 17:58:25 +05:30
import { useState, useEffect, useCallback, useMemo } from "react";
2026-05-18 16:41:38 +05:30
import toast from "react-hot-toast";
2026-05-15 17:58:25 +05:30
import { AxiosError } from "axios";
2026-05-18 11:55:55 +05:30
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
2026-05-15 17:58:25 +05:30
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<HealthPackage[]>([]);
const [categories, setCategories] = useState<HealthCategory[]>([]);
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<HealthPackage | null>(
null,
);
const [editingPackage, setEditingPackage] = useState<HealthPackage | null>(
null,
);
const [editingCategory, setEditingCategory] = useState<HealthCategory | null>(
null,
);
// Filters & Pagination
const [searchText, setSearchText] = useState("");
const [filterCategory, setFilterCategory] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
// Forms
const [pkgForm, setPkgForm] = useState<Partial<HealthPackage>>({
name: "",
slug: "",
description: "",
2026-05-18 11:55:55 +05:30
image: "",
2026-05-15 17:58:25 +05:30
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 });
2026-05-18 16:41:38 +05:30
toast.success(`Package ${pkg.isActive ? "hidden" : "activated"}`);
2026-05-15 17:58:25 +05:30
fetchData();
} catch (err) {
console.error("Failed to update status", err);
2026-05-18 16:41:38 +05:30
toast.error("Failed to update status");
2026-05-15 17:58:25 +05:30
}
};
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 });
2026-05-18 16:41:38 +05:30
toast.success(`Category ${cat.isActive ? "hidden" : "activated"}`);
2026-05-15 17:58:25 +05:30
fetchData();
} catch (err) {
console.error("Failed to update category status", err);
2026-05-18 16:41:38 +05:30
toast.error("Failed to update category status");
2026-05-15 17:58:25 +05:30
}
};
const openAddPackage = () => {
2026-05-18 16:41:38 +05:30
if (categories.length === 0) {
toast.error(
"Please create at least one category before attempting to add a health package.",
);
return;
}
2026-05-15 17:58:25 +05:30
setEditingPackage(null);
setPkgForm({
name: "",
slug: "",
description: "",
2026-05-18 11:55:55 +05:30
image: "",
2026-05-15 17:58:25 +05:30
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 () => {
2026-05-18 16:41:38 +05:30
if (!pkgForm.image) return toast.error("Package image is required.");
if (!pkgForm.name?.trim()) return toast.error("Package Name is required.");
if (!pkgForm.slug?.trim()) return toast.error("URL Slug is required.");
if (!pkgForm.categoryId)
return toast.error("Please select a valid category.");
if (pkgForm.price === undefined || pkgForm.price <= 0)
return toast.error(
"Regular Price must be a valid amount greater than 0.",
);
if (!pkgForm.description?.trim())
return toast.error("Description is required.");
const structureFilled = inclusionsList.some(
(item) => item.category.trim() !== "" && item.items.trim() !== "",
);
if (!structureFilled) {
return toast.error(
"Please provide at least one valid Category Group with tests inside it.",
);
}
2026-05-15 17:58:25 +05:30
try {
// Convert the dynamic array back into the required JSON object format
const parsedInclusions: Record<string, string[]> = {};
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<string, any> = {};
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);
2026-05-18 16:41:38 +05:30
toast.success("Package updated successfully!");
2026-05-15 17:58:25 +05:30
} else {
await createHealthPackageApi(finalData);
2026-05-18 16:41:38 +05:30
toast.success("Package created successfully!");
2026-05-15 17:58:25 +05:30
}
setPackageModal(false);
fetchData();
} catch (err) {
console.error(err);
2026-05-18 16:41:38 +05:30
toast.error(
"An unexpected system error occurred while trying to save the package.",
);
2026-05-15 17:58:25 +05:30
}
};
const saveCategory = async () => {
2026-05-18 16:41:38 +05:30
if (!catForm.name?.trim()) return toast.error("Category Name is required.");
2026-05-15 17:58:25 +05:30
try {
if (editingCategory?.id) {
const changedFields: Record<string, any> = {};
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<HealthCategory>,
);
2026-05-18 16:41:38 +05:30
toast.success("Category updated successfully!");
2026-05-15 17:58:25 +05:30
} else {
await createCategoryApi(catForm as any);
2026-05-18 16:41:38 +05:30
toast.success("Category created successfully!");
2026-05-15 17:58:25 +05:30
}
setCategoryModal(false);
fetchData();
} catch (err) {
console.error(err);
2026-05-18 16:41:38 +05:30
toast.error("An error occurred while saving the category.");
2026-05-15 17:58:25 +05:30
}
};
const deleteCategory = async (id: number) => {
if (confirm("Delete this category? Ensure no packages are linked to it.")) {
2026-05-18 16:41:38 +05:30
try {
await deleteCategoryApi(id);
toast.success("Category deleted successfully!");
fetchData();
} catch (err) {
console.error(err);
toast.error("Failed to delete category.");
}
2026-05-15 17:58:25 +05:30
}
};
return (
<div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<h1 className="text-3xl font-bold">Health Packages</h1>
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search packages..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[250px] text-base"
/>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
2026-05-18 11:55:55 +05:30
className="flex h-10 w-[220px] rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
2026-05-15 17:58:25 +05:30
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
<Button
variant="outline"
onClick={fetchData}
disabled={loading}
2026-05-18 11:55:55 +05:30
className="text-base"
>
2026-05-15 17:58:25 +05:30
<RefreshCw className="mr-2 h-5 w-5" />
Refresh
</Button>
<Button onClick={openAddPackage} className="text-base">
<Plus className="mr-2 h-5 w-5" />
Add Package
</Button>
</div>
</div>
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md text-base">
{error}
</div>
)}
<Tabs defaultValue="packages" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="packages">Packages</TabsTrigger>
<TabsTrigger value="categories">Categories</TabsTrigger>
<TabsTrigger value="inquiries">Inquiries</TabsTrigger>
</TabsList>
{/* PACKAGES TAB */}
<TabsContent value="packages">
<Card>
<CardHeader>
<CardTitle className="text-xl">Package List</CardTitle>
</CardHeader>
<CardContent className="p-0 sm:p-6 space-y-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1000px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[80px] bg-background text-sm font-bold">
Priority
</TableHead>
<TableHead className="w-[250px] bg-background text-sm font-bold">
Package Details
</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">
Category
</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">
Pricing
</TableHead>
<TableHead className="w-[120px] bg-background text-sm font-bold">
Status
</TableHead>
<TableHead className="w-[120px] bg-background text-right text-sm font-bold">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : currentItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
2026-05-18 11:55:55 +05:30
className="text-center text-muted-foreground py-10 text-base"
>
2026-05-15 17:58:25 +05:30
No packages found
</TableCell>
</TableRow>
) : (
currentItems.map((pkg) => (
<TableRow key={pkg.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-sm">
{pkg.sortOrder}
</TableCell>
<TableCell>
<div
className="font-semibold text-base truncate"
2026-05-18 11:55:55 +05:30
title={pkg.name}
>
2026-05-15 17:58:25 +05:30
{pkg.name}
</div>
<div className="text-xs text-muted-foreground truncate font-mono mt-0.5">
/{pkg.slug}
</div>
</TableCell>
<TableCell>
<Badge variant="secondary" className="text-xs">
{pkg.category?.name}
</Badge>
</TableCell>
<TableCell>
<div className="font-semibold">
{pkg.discountedPrice || pkg.price}
</div>
{pkg.discountedPrice &&
pkg.discountedPrice < pkg.price && (
<div className="text-xs text-muted-foreground line-through">
{pkg.price}
</div>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={pkg.isActive}
onCheckedChange={() => handleToggleStatus(pkg)}
/>
<Badge
2026-05-18 11:55:55 +05:30
variant={pkg.isActive ? "default" : "secondary"}
>
2026-05-15 17:58:25 +05:30
{pkg.isActive ? "Active" : "Hidden"}
</Badge>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={() => {
setSelectedPackage(pkg);
setViewModal(true);
2026-05-18 11:55:55 +05:30
}}
>
2026-05-15 17:58:25 +05:30
<Eye className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
2026-05-18 11:55:55 +05:30
onClick={() => openEditPackage(pkg)}
>
2026-05-15 17:58:25 +05:30
<Pencil className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loading && filteredPackages.length > 0 && (
<div className="flex items-center justify-between px-2 py-6 border-t">
<div className="text-base text-muted-foreground">
Showing{" "}
<span className="font-semibold">
{indexOfFirstItem + 1}
</span>{" "}
to{" "}
<span className="font-semibold">
{Math.min(indexOfLastItem, filteredPackages.length)}
</span>{" "}
of{" "}
<span className="font-semibold">
{filteredPackages.length}
</span>{" "}
packages
</div>
<div className="flex items-center gap-6">
<div className="text-base font-semibold">
Page {currentPage} of {totalPages}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="h-10 w-10"
onClick={() =>
setCurrentPage((prev) => Math.max(prev - 1, 1))
}
2026-05-18 11:55:55 +05:30
disabled={currentPage === 1}
>
2026-05-15 17:58:25 +05:30
<ChevronLeft className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-10 w-10"
onClick={() =>
setCurrentPage((prev) =>
Math.min(prev + 1, totalPages),
)
}
disabled={
currentPage === totalPages || totalPages === 0
2026-05-18 11:55:55 +05:30
}
>
2026-05-15 17:58:25 +05:30
<ChevronRight className="h-5 w-5" />
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* CATEGORIES TAB */}
<TabsContent value="categories">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-xl">Category List</CardTitle>
<Button
size="sm"
onClick={() => {
setEditingCategory(null);
setCatForm({
name: "",
slug: "",
sortOrder: 1000,
isActive: true,
});
setCategoryModal(true);
2026-05-18 11:55:55 +05:30
}}
>
2026-05-15 17:58:25 +05:30
<LayoutGrid className="mr-2 h-4 w-4" /> Add Category
</Button>
</CardHeader>
<CardContent className="p-0 sm:p-6">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[100px] bg-background text-sm font-bold">
Priority
</TableHead>
<TableHead className="bg-background text-sm font-bold">
Category Name
</TableHead>
<TableHead className="w-[100px] bg-background text-sm font-bold">
Status
</TableHead>
<TableHead className="w-[100px] bg-background text-right text-sm font-bold">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.map((cat) => (
<TableRow key={cat.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-sm">
{cat.sortOrder}
</TableCell>
<TableCell className="font-semibold text-base">
{cat.name}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={cat.isActive}
onCheckedChange={() =>
handleToggleCategoryStatus(cat)
}
/>
<Badge
2026-05-18 11:55:55 +05:30
variant={cat.isActive ? "default" : "secondary"}
>
2026-05-15 17:58:25 +05:30
{cat.isActive ? "Active" : "Hidden"}
</Badge>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
className="h-9 w-9"
onClick={() => {
setEditingCategory(cat);
setCatForm(cat as any);
setCategoryModal(true);
2026-05-18 11:55:55 +05:30
}}
>
2026-05-15 17:58:25 +05:30
<Pencil className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="inquiries">
<PackageInquiriesTab />
</TabsContent>
</Tabs>
{/* --- PACKAGE MODAL --- */}
<Dialog open={packageModal} onOpenChange={setPackageModal}>
<DialogContent className="w-full !max-w-5xl h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="p-6 border-b bg-background z-10">
<DialogTitle className="text-2xl">
{editingPackage ? "Edit Package" : "Add Package"}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Profile & Pricing
</h3>
<div className="space-y-4">
2026-05-18 11:55:55 +05:30
<div className="space-y-2">
<Label className="text-sm font-semibold">
2026-05-18 13:15:00 +05:30
Package Image(Dimensions: 650w x 250h)
2026-05-18 11:55:55 +05:30
</Label>
<BytescaleUploader
value={pkgForm.image || ""}
folderPath="/health-packages"
onChange={(url) =>
setPkgForm({
...pkgForm,
image: url,
})
}
/>
</div>
2026-05-15 17:58:25 +05:30
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
<Label className="text-base font-semibold cursor-pointer">
Active Visibility
</Label>
<Switch
checked={pkgForm.isActive}
onCheckedChange={(val) =>
setPkgForm({ ...pkgForm, isActive: val })
}
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">
Sort Priority (Lower numbers show first)
</Label>
<Input
type="number"
value={pkgForm.sortOrder}
onChange={(e) =>
setPkgForm({
...pkgForm,
sortOrder: Number(e.target.value),
})
}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">
Package Name
</Label>
<Input
value={pkgForm.name}
onChange={(e) =>
setPkgForm({ ...pkgForm, name: e.target.value })
}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">URL Slug</Label>
<Input
value={pkgForm.slug}
onChange={(e) =>
setPkgForm({ ...pkgForm, slug: e.target.value })
}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">Category</Label>
<Select
value={pkgForm.categoryId?.toString()}
onValueChange={(v) =>
setPkgForm({ ...pkgForm, categoryId: Number(v) })
2026-05-18 11:55:55 +05:30
}
>
2026-05-15 17:58:25 +05:30
<SelectTrigger className="text-base">
<SelectValue />
</SelectTrigger>
<SelectContent>
{categories.map((c) => (
<SelectItem key={c.id} value={c.id.toString()}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<Label className="text-sm font-semibold">
Regular Price ()
</Label>
<Input
type="number"
value={pkgForm.price || ""}
onChange={(e) =>
setPkgForm({
...pkgForm,
price: Number(e.target.value),
})
}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">
Discounted Price ()
</Label>
<Input
type="number"
value={pkgForm.discountedPrice || ""}
onChange={(e) =>
setPkgForm({
...pkgForm,
discountedPrice: Number(e.target.value),
})
}
className="text-base"
/>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Details & Inclusions
</h3>
<div className="space-y-4">
<div className="space-y-1">
<Label className="text-sm font-semibold">Description</Label>
<Textarea
rows={3}
value={pkgForm.description}
onChange={(e) =>
setPkgForm({ ...pkgForm, description: e.target.value })
}
className="text-base"
/>
</div>
<div className="space-y-1">
<div className="space-y-4">
<div className="flex items-center justify-between mb-2">
<Label className="text-sm font-semibold">
Tests & Inclusions
</Label>
<Badge variant="outline">Grouped Fields</Badge>
</div>
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2">
{inclusionsList.map((inc) => (
<div
key={inc.id}
2026-05-18 11:55:55 +05:30
className="p-4 border rounded-md bg-muted/10 relative"
>
2026-05-15 17:58:25 +05:30
{/* Remove Button */}
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50"
2026-05-18 11:55:55 +05:30
onClick={() => handleRemoveInclusionField(inc.id)}
>
2026-05-15 17:58:25 +05:30
<Trash2 className="h-4 w-4" />
</Button>
<div className="space-y-3 pr-8">
<div className="space-y-1">
<Label className="text-xs text-muted-foreground uppercase font-bold">
Category Title
</Label>
<Input
placeholder="e.g. Routine Blood Tests"
value={inc.category}
onChange={(e) =>
handleUpdateInclusionField(
inc.id,
"category",
e.target.value,
)
}
className="font-semibold text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground uppercase font-bold">
Included Tests
</Label>
<p className="text-[10px] text-muted-foreground leading-none mb-1">
Separate tests with a comma (,)
</p>
<Textarea
rows={2}
placeholder="e.g. CBC, LFT, RFT, TSH"
value={inc.items}
onChange={(e) =>
handleUpdateInclusionField(
inc.id,
"items",
e.target.value,
)
}
className="text-sm"
/>
</div>
</div>
</div>
))}
</div>
<Button
variant="outline"
className="w-full mt-2 border-dashed border-2"
2026-05-18 11:55:55 +05:30
onClick={handleAddInclusionField}
>
2026-05-15 17:58:25 +05:30
<Plus className="mr-2 h-4 w-4" />
Add New Category Group
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t">
<Button
variant="ghost"
onClick={() => setPackageModal(false)}
2026-05-18 11:55:55 +05:30
className="text-base"
>
2026-05-15 17:58:25 +05:30
Cancel
</Button>
<Button onClick={savePackage} className="px-10 text-base">
{editingPackage ? "Save Changes" : "Create Package"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* --- CATEGORY MODAL --- */}
<Dialog open={categoryModal} onOpenChange={setCategoryModal}>
<DialogContent className="w-full !max-w-2xl flex flex-col p-0 overflow-hidden">
<DialogHeader className="p-6 border-b">
<DialogTitle>
{editingCategory ? "Edit Category" : "Add Category"}
</DialogTitle>
</DialogHeader>
<div className="p-6 space-y-4">
<div className="space-y-1">
<Label className="text-sm font-semibold">Category Name</Label>
<Input
value={catForm.name}
onChange={(e) =>
setCatForm({ ...catForm, name: e.target.value })
}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">Sort Order</Label>
<Input
type="number"
value={catForm.sortOrder}
onChange={(e) =>
setCatForm({ ...catForm, sortOrder: Number(e.target.value) })
}
className="text-base"
/>
</div>
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30 mb-2">
<Label className="text-base font-semibold cursor-pointer">
Active Visibility
</Label>
<Switch
checked={catForm.isActive}
onCheckedChange={(val) =>
setCatForm({ ...catForm, isActive: val })
}
/>
</div>
</div>
<DialogFooter className="p-6 border-t">
<Button variant="ghost" onClick={() => setCategoryModal(false)}>
Cancel
</Button>
<Button onClick={saveCategory}>
{editingCategory ? "Save Category" : "Create Category"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* --- VIEW MODAL --- */}
<Dialog open={viewModal} onOpenChange={setViewModal}>
<DialogContent className="w-full !max-w-5xl h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="p-6 border-b bg-background z-10">
<DialogTitle className="text-2xl">
{selectedPackage?.name}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="flex justify-between items-center bg-muted/20 p-4 rounded-lg">
<div>
<p className="text-sm text-muted-foreground uppercase font-bold">
Category
</p>
<p className="text-base font-medium">
{selectedPackage?.category?.name}
</p>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground uppercase font-bold">
Pricing
</p>
<p className="text-xl font-bold">
{selectedPackage?.discountedPrice || selectedPackage?.price}
</p>
</div>
</div>
<div>
<h3 className="font-bold text-base border-b pb-2 mb-4">
Inclusions
</h3>
<div className="space-y-6">
{selectedPackage?.inclusions &&
typeof selectedPackage.inclusions === "object" &&
!Array.isArray(selectedPackage.inclusions) ? (
Object.entries(selectedPackage.inclusions).map(
([category, tests], idx) => (
<div key={idx}>
<h4 className="font-semibold text-sm text-primary mb-3 uppercase tracking-wider">
{category}
</h4>
<div className="grid grid-cols-2 gap-3">
{Array.isArray(tests) &&
tests.map((item, i) => (
<div
key={i}
2026-05-18 11:55:55 +05:30
className="text-sm border p-3 rounded bg-background shadow-sm"
>
2026-05-15 17:58:25 +05:30
{item}
</div>
))}
</div>
</div>
),
)
) : (
<div className="grid grid-cols-2 gap-3">
{Array.isArray(selectedPackage?.inclusions) &&
selectedPackage.inclusions.map((item, i) => (
<div
key={i}
2026-05-18 11:55:55 +05:30
className="text-sm border p-3 rounded bg-background shadow-sm"
>
2026-05-15 17:58:25 +05:30
{item}
</div>
))}
</div>
)}
</div>
</div>
</div>
<DialogFooter className="p-6 border-t">
<Button onClick={() => setViewModal(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}