437 lines
11 KiB
TypeScript
437 lines
11 KiB
TypeScript
|
|
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
|
|||
|
|
import SeoFields from "@/components/SeoFields/SeoFields";
|
|||
|
|
import { useEffect } from "react";
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Dialog,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogFooter,
|
|||
|
|
DialogHeader,
|
|||
|
|
DialogTitle,
|
|||
|
|
} from "@/components/ui/dialog";
|
|||
|
|
|
|||
|
|
import { Input } from "@/components/ui/input";
|
|||
|
|
import { Textarea } from "@/components/ui/textarea";
|
|||
|
|
import { Button } from "@/components/ui/button";
|
|||
|
|
import { Label } from "@/components/ui/label";
|
|||
|
|
import { Badge } from "@/components/ui/badge";
|
|||
|
|
import { Switch } from "@/components/ui/switch";
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Select,
|
|||
|
|
SelectContent,
|
|||
|
|
SelectItem,
|
|||
|
|
SelectTrigger,
|
|||
|
|
SelectValue,
|
|||
|
|
} from "@/components/ui/select";
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Accordion,
|
|||
|
|
AccordionContent,
|
|||
|
|
AccordionItem,
|
|||
|
|
AccordionTrigger,
|
|||
|
|
} from "@/components/ui/accordion";
|
|||
|
|
|
|||
|
|
import { Plus, Trash2 } from "lucide-react";
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
open: boolean;
|
|||
|
|
onOpenChange: (open: boolean) => void;
|
|||
|
|
editingPackage: any;
|
|||
|
|
pkgForm: any;
|
|||
|
|
setPkgForm: any;
|
|||
|
|
inclusionsList: any[];
|
|||
|
|
setInclusionsList: any;
|
|||
|
|
categories: any[];
|
|||
|
|
onSave: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function HealthPackageModal({
|
|||
|
|
open,
|
|||
|
|
onOpenChange,
|
|||
|
|
editingPackage,
|
|||
|
|
pkgForm,
|
|||
|
|
setPkgForm,
|
|||
|
|
inclusionsList,
|
|||
|
|
setInclusionsList,
|
|||
|
|
categories,
|
|||
|
|
onSave,
|
|||
|
|
}: Props) {
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!editingPackage && pkgForm.name) {
|
|||
|
|
setPkgForm((prev: any) => ({
|
|||
|
|
...prev,
|
|||
|
|
slug: prev.slug
|
|||
|
|
? prev.slug
|
|||
|
|
: pkgForm.name
|
|||
|
|
.toLowerCase()
|
|||
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|||
|
|
.replace(/(^-|-$)/g, ""),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
}, [pkgForm.name]);
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|||
|
|
<DialogContent className="w-full !max-w-7xl h-[92vh] flex flex-col p-0 overflow-hidden">
|
|||
|
|
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
|
|||
|
|
<DialogTitle className="text-2xl font-bold">
|
|||
|
|
{editingPackage ? "Edit Health Package" : "Create Health Package"}
|
|||
|
|
</DialogTitle>
|
|||
|
|
</DialogHeader>
|
|||
|
|
|
|||
|
|
<div className="flex-1 overflow-y-auto">
|
|||
|
|
<div className="grid grid-cols-1 xl:grid-cols-[1.2fr_0.8fr] gap-8 p-6">
|
|||
|
|
{/* LEFT COLUMN */}
|
|||
|
|
<div className="space-y-8">
|
|||
|
|
<div className="space-y-5">
|
|||
|
|
<div className="sticky top-0 bg-background z-10 pb-2">
|
|||
|
|
<h3 className="text-lg font-bold">Profile & Pricing</h3>
|
|||
|
|
|
|||
|
|
<p className="text-sm text-muted-foreground">
|
|||
|
|
Main package information
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-5">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">Package Image</Label>
|
|||
|
|
|
|||
|
|
<p className="text-xs text-muted-foreground">
|
|||
|
|
Recommended size: 650 × 250
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<BytescaleUploader
|
|||
|
|
value={pkgForm.image || ""}
|
|||
|
|
folderPath="/health-packages"
|
|||
|
|
onChange={(url) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
image: url,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
|
|||
|
|
<div>
|
|||
|
|
<p className="font-semibold">Active Visibility</p>
|
|||
|
|
|
|||
|
|
<p className="text-sm text-muted-foreground">
|
|||
|
|
Show this package publicly
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Switch
|
|||
|
|
checked={pkgForm.isActive}
|
|||
|
|
onCheckedChange={(val) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
isActive: val,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">Package Name</Label>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
value={pkgForm.name}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
name: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">URL Slug</Label>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
value={pkgForm.slug}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
slug: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">Category</Label>
|
|||
|
|
|
|||
|
|
<Select
|
|||
|
|
value={pkgForm.categoryId?.toString()}
|
|||
|
|
onValueChange={(v) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
categoryId: Number(v),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger>
|
|||
|
|
<SelectValue placeholder="Select category" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
|
|||
|
|
<SelectContent>
|
|||
|
|
{categories.map((c) => (
|
|||
|
|
<SelectItem key={c.id} value={c.id.toString()}>
|
|||
|
|
{c.name}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="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,
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="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,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">Sort Priority</Label>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
type="number"
|
|||
|
|
value={pkgForm.sortOrder}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
sortOrder: Number(e.target.value),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="font-semibold">Description</Label>
|
|||
|
|
|
|||
|
|
<Textarea
|
|||
|
|
rows={5}
|
|||
|
|
value={pkgForm.description}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
description: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* INCLUSIONS */}
|
|||
|
|
<div className="space-y-5">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-lg font-bold">Tests & Inclusions</h3>
|
|||
|
|
|
|||
|
|
<p className="text-sm text-muted-foreground">
|
|||
|
|
Group tests into categories
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Badge variant="outline">
|
|||
|
|
{inclusionsList.length} Groups
|
|||
|
|
</Badge>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Accordion type="multiple" className="space-y-4">
|
|||
|
|
{inclusionsList.map((inc, index) => {
|
|||
|
|
const testCount = inc.items
|
|||
|
|
?.split(",")
|
|||
|
|
.filter(Boolean).length;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AccordionItem
|
|||
|
|
key={inc.id}
|
|||
|
|
value={inc.id.toString()}
|
|||
|
|
className="border rounded-xl bg-background px-5 shadow-sm"
|
|||
|
|
>
|
|||
|
|
<AccordionTrigger className="hover:no-underline w-full">
|
|||
|
|
<div className="flex w-full items-center justify-between">
|
|||
|
|
<div className="flex flex-col items-start text-left">
|
|||
|
|
<p className="font-semibold">
|
|||
|
|
{inc.category || `Group ${index + 1}`}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<p className="text-xs text-muted-foreground">
|
|||
|
|
{testCount || 0} tests included
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Button
|
|||
|
|
variant="ghost"
|
|||
|
|
size="sm"
|
|||
|
|
className="text-red-500 hover:text-red-600"
|
|||
|
|
onClick={() => handleRemoveInclusionField(inc.id)}
|
|||
|
|
>
|
|||
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|||
|
|
Remove
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</AccordionTrigger>
|
|||
|
|
|
|||
|
|
<AccordionContent className="pt-4">
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label>Category Title</Label>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
placeholder="Routine Blood Tests"
|
|||
|
|
value={inc.category}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleUpdateInclusionField(
|
|||
|
|
inc.id,
|
|||
|
|
"category",
|
|||
|
|
e.target.value,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<Label>Included Tests</Label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Textarea
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="CBC, LFT, RFT, TSH"
|
|||
|
|
value={inc.items}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleUpdateInclusionField(
|
|||
|
|
inc.id,
|
|||
|
|
"items",
|
|||
|
|
e.target.value,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<p className="text-xs text-muted-foreground">
|
|||
|
|
Separate each test using commas
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</AccordionContent>
|
|||
|
|
</AccordionItem>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</Accordion>
|
|||
|
|
|
|||
|
|
<Button
|
|||
|
|
variant="outline"
|
|||
|
|
className="w-full border-dashed border-2 h-12"
|
|||
|
|
onClick={handleAddInclusionField}
|
|||
|
|
>
|
|||
|
|
<Plus className="h-4 w-4 mr-2" />
|
|||
|
|
Add New Inclusion Group
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* RIGHT COLUMN */}
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<SeoFields
|
|||
|
|
value={pkgForm.seo}
|
|||
|
|
slug={pkgForm.slug}
|
|||
|
|
folderPath="/seo"
|
|||
|
|
onChange={(seo) =>
|
|||
|
|
setPkgForm({
|
|||
|
|
...pkgForm,
|
|||
|
|
seo,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
|
|||
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
|||
|
|
Cancel
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
<Button className="px-10" onClick={onSave}>
|
|||
|
|
{editingPackage ? "Save Changes" : "Create Package"}
|
|||
|
|
</Button>
|
|||
|
|
</DialogFooter>
|
|||
|
|
</DialogContent>
|
|||
|
|
</Dialog>
|
|||
|
|
);
|
|||
|
|
}
|