feat: health check seo

This commit is contained in:
Kailasdevdas
2026-05-26 11:56:22 +05:30
parent 31c0e50177
commit 4d73da5ddd
9 changed files with 886 additions and 325 deletions
+33 -315
View File
@@ -1,13 +1,12 @@
import { useState, useEffect, useCallback, useMemo } from "react";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
import {
getHealthPackagesApi,
getHealthCategoriesApi,
createHealthPackageApi,
updateHealthPackageApi,
createHealthPackageApi,
createCategoryApi,
updateCategoryApi,
deleteCategoryApi,
@@ -16,6 +15,7 @@ import {
} from "@/api/healthCheck";
import PackageInquiriesTab from "@/components/PackageInquiriesTab/PackageInquiriesTab";
import HealthPackageModal from "@/components/HealthPackageModal/HealthPackageModal";
import {
Table,
@@ -35,18 +35,10 @@ import {
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,
@@ -57,7 +49,6 @@ import {
ChevronRight,
LayoutGrid,
Eye,
Trash2,
} from "lucide-react";
export default function HealthPackagePage() {
@@ -99,6 +90,15 @@ export default function HealthPackagePage() {
categoryId: 0,
isActive: true,
sortOrder: 1000,
seo: {
seoTitle: "",
metaDescription: "",
focusKeyphrase: "",
tags: [],
ogTitle: "",
ogDescription: "",
ogImage: "",
},
});
const [inclusionsList, setInclusionsList] = useState([
{ id: Date.now(), category: "", items: "" },
@@ -209,6 +209,15 @@ export default function HealthPackagePage() {
categoryId: categories[0]?.id || 0,
isActive: true,
sortOrder: 1000,
seo: {
seoTitle: "",
metaDescription: "",
focusKeyphrase: "",
tags: [],
ogTitle: "",
ogDescription: "",
ogImage: "",
},
});
setInclusionsList([{ id: Date.now(), category: "", items: "" }]);
setPackageModal(true);
@@ -241,28 +250,6 @@ export default function HealthPackagePage() {
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 () => {
if (!pkgForm.image) return toast.error("Package image is required.");
@@ -283,7 +270,6 @@ export default function HealthPackagePage() {
}
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();
@@ -310,6 +296,7 @@ export default function HealthPackagePage() {
finalData.discountedPrice !== null
? Number(finalData.discountedPrice)
: null;
if (editingPackage?.id) {
const changedFields: Record<string, any> = {};
Object.keys(finalData).forEach((key) => {
@@ -382,19 +369,6 @@ export default function HealthPackagePage() {
}
};
const deleteCategory = async (id: number) => {
if (confirm("Delete this category? Ensure no packages are linked to it.")) {
try {
await deleteCategoryApi(id);
toast.success("Category deleted successfully!");
fetchData();
} catch (err) {
console.error(err);
toast.error("Failed to delete category.");
}
}
};
return (
<div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
@@ -730,274 +704,18 @@ export default function HealthPackagePage() {
</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">
<div className="space-y-2">
<Label className="text-sm font-semibold">
Package Image(Dimensions: 650w x 250h)
</Label>
<BytescaleUploader
value={pkgForm.image || ""}
folderPath="/health-packages"
onChange={(url) =>
setPkgForm({
...pkgForm,
image: url,
})
}
/>
</div>
<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) })
}
>
<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) => {
const value = e.target.value
? Number(e.target.value)
: undefined;
setPkgForm({
...pkgForm,
price: value,
discountedPrice: value
? pkgForm.discountedPrice
: undefined,
});
}}
className="text-base"
/>
</div>
<div className="space-y-1">
<Label className="text-sm font-semibold">
Discounted Price ()
</Label>
<Input
type="number"
disabled={!pkgForm.price}
value={pkgForm.discountedPrice || ""}
onChange={(e) =>
setPkgForm({
...pkgForm,
discountedPrice: e.target.value
? Number(e.target.value)
: undefined,
})
}
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}
className="p-4 border rounded-md bg-muted/10 relative"
>
{/* 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"
onClick={() => handleRemoveInclusionField(inc.id)}
>
<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"
onClick={handleAddInclusionField}
>
<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)}
className="text-base"
>
Cancel
</Button>
<Button onClick={savePackage} className="px-10 text-base">
{editingPackage ? "Save Changes" : "Create Package"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* --- REPLACED MODAL CONTAINER --- */}
<HealthPackageModal
open={packageModal}
onOpenChange={setPackageModal}
editingPackage={editingPackage}
pkgForm={pkgForm}
setPkgForm={setPkgForm}
inclusionsList={inclusionsList}
setInclusionsList={setInclusionsList}
categories={categories}
onSave={savePackage}
/>
{/* --- CATEGORY MODAL --- */}
<Dialog open={categoryModal} onOpenChange={setCategoryModal}>