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>
|
|
|
|
|
);
|
|
|
|
|
}
|