feat: health check seo
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user