feat: add image upload for health package
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
|
||||||
@@ -241,6 +241,7 @@ model HealthPackage {
|
|||||||
slug String @unique
|
slug String @unique
|
||||||
description String?
|
description String?
|
||||||
price Decimal? @db.Decimal(10, 2)
|
price Decimal? @db.Decimal(10, 2)
|
||||||
|
image String?
|
||||||
discountedPrice Decimal? @db.Decimal(10, 2)
|
discountedPrice Decimal? @db.Decimal(10, 2)
|
||||||
|
|
||||||
inclusions Json @default("{}")
|
inclusions Json @default("{}")
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const createPackage = async (req, res) => {
|
|||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
price,
|
price,
|
||||||
|
image,
|
||||||
discountedPrice,
|
discountedPrice,
|
||||||
inclusions,
|
inclusions,
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -155,6 +156,7 @@ export const createPackage = async (req, res) => {
|
|||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
price,
|
price,
|
||||||
|
image,
|
||||||
discountedPrice,
|
discountedPrice,
|
||||||
inclusions,
|
inclusions,
|
||||||
categoryId: Number(categoryId),
|
categoryId: Number(categoryId),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface HealthPackage {
|
|||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
image?: string;
|
||||||
discountedPrice?: number;
|
discountedPrice?: number;
|
||||||
inclusions: Record<string, string[]>;
|
inclusions: Record<string, string[]>;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import {useState, useRef} from "react";
|
import { useState, useRef } from "react";
|
||||||
import {Button} from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {User, X, Loader2} from "lucide-react";
|
import { User, X, Loader2 } from "lucide-react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface BytescaleUploaderProps {
|
interface BytescaleUploaderProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (url: string) => void;
|
onChange: (url: string) => void;
|
||||||
folderPath: "/doctors" | "/departments" | "/news" | "/blog";
|
folderPath:
|
||||||
|
| "/doctors"
|
||||||
|
| "/departments"
|
||||||
|
| "/news"
|
||||||
|
| "/blog"
|
||||||
|
| "/health-packages";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BytescaleUploader({
|
export function BytescaleUploader({
|
||||||
@@ -40,7 +45,7 @@ export function BytescaleUploader({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {fileUrl} = response.data;
|
const { fileUrl } = response.data;
|
||||||
onChange(fileUrl);
|
onChange(fileUrl);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Upload Error:", e);
|
console.error("Upload Error:", e);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getHealthPackagesApi,
|
getHealthPackagesApi,
|
||||||
@@ -91,6 +92,7 @@ export default function HealthPackagePage() {
|
|||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
image: "",
|
||||||
price: 0,
|
price: 0,
|
||||||
discountedPrice: 0,
|
discountedPrice: 0,
|
||||||
categoryId: 0,
|
categoryId: 0,
|
||||||
@@ -190,6 +192,7 @@ export default function HealthPackagePage() {
|
|||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
image: "",
|
||||||
price: 0,
|
price: 0,
|
||||||
discountedPrice: 0,
|
discountedPrice: 0,
|
||||||
categoryId: categories[0]?.id || 0,
|
categoryId: categories[0]?.id || 0,
|
||||||
@@ -355,7 +358,8 @@ export default function HealthPackagePage() {
|
|||||||
<select
|
<select
|
||||||
value={filterCategory}
|
value={filterCategory}
|
||||||
onChange={(e) => setFilterCategory(e.target.value)}
|
onChange={(e) => setFilterCategory(e.target.value)}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<option key={cat.id} value={cat.id}>
|
<option key={cat.id} value={cat.id}>
|
||||||
@@ -368,7 +372,8 @@ export default function HealthPackagePage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={fetchData}
|
onClick={fetchData}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="text-base">
|
className="text-base"
|
||||||
|
>
|
||||||
<RefreshCw className="mr-2 h-5 w-5" />
|
<RefreshCw className="mr-2 h-5 w-5" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
@@ -435,7 +440,8 @@ export default function HealthPackagePage() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6}
|
colSpan={6}
|
||||||
className="text-center text-muted-foreground py-10 text-base">
|
className="text-center text-muted-foreground py-10 text-base"
|
||||||
|
>
|
||||||
No packages found
|
No packages found
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -448,7 +454,8 @@ export default function HealthPackagePage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
className="font-semibold text-base truncate"
|
className="font-semibold text-base truncate"
|
||||||
title={pkg.name}>
|
title={pkg.name}
|
||||||
|
>
|
||||||
{pkg.name}
|
{pkg.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground truncate font-mono mt-0.5">
|
<div className="text-xs text-muted-foreground truncate font-mono mt-0.5">
|
||||||
@@ -478,9 +485,8 @@ export default function HealthPackagePage() {
|
|||||||
onCheckedChange={() => handleToggleStatus(pkg)}
|
onCheckedChange={() => handleToggleStatus(pkg)}
|
||||||
/>
|
/>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={pkg.isActive ? "default" : "secondary"}
|
||||||
pkg.isActive ? "default" : "secondary"
|
>
|
||||||
}>
|
|
||||||
{pkg.isActive ? "Active" : "Hidden"}
|
{pkg.isActive ? "Active" : "Hidden"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -494,14 +500,16 @@ export default function HealthPackagePage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedPackage(pkg);
|
setSelectedPackage(pkg);
|
||||||
setViewModal(true);
|
setViewModal(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-9 w-9"
|
className="h-9 w-9"
|
||||||
onClick={() => openEditPackage(pkg)}>
|
onClick={() => openEditPackage(pkg)}
|
||||||
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -542,7 +550,8 @@ export default function HealthPackagePage() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCurrentPage((prev) => Math.max(prev - 1, 1))
|
setCurrentPage((prev) => Math.max(prev - 1, 1))
|
||||||
}
|
}
|
||||||
disabled={currentPage === 1}>
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -556,7 +565,8 @@ export default function HealthPackagePage() {
|
|||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
currentPage === totalPages || totalPages === 0
|
currentPage === totalPages || totalPages === 0
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<ChevronRight className="h-5 w-5" />
|
<ChevronRight className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -583,7 +593,8 @@ export default function HealthPackagePage() {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
setCategoryModal(true);
|
setCategoryModal(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<LayoutGrid className="mr-2 h-4 w-4" /> Add Category
|
<LayoutGrid className="mr-2 h-4 w-4" /> Add Category
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -625,7 +636,8 @@ export default function HealthPackagePage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Badge
|
<Badge
|
||||||
variant={cat.isActive ? "default" : "secondary"}>
|
variant={cat.isActive ? "default" : "secondary"}
|
||||||
|
>
|
||||||
{cat.isActive ? "Active" : "Hidden"}
|
{cat.isActive ? "Active" : "Hidden"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -640,7 +652,8 @@ export default function HealthPackagePage() {
|
|||||||
setEditingCategory(cat);
|
setEditingCategory(cat);
|
||||||
setCatForm(cat as any);
|
setCatForm(cat as any);
|
||||||
setCategoryModal(true);
|
setCategoryModal(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,6 +687,22 @@ export default function HealthPackagePage() {
|
|||||||
Profile & Pricing
|
Profile & Pricing
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
Package Image
|
||||||
|
</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">
|
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
|
||||||
<Label className="text-base font-semibold cursor-pointer">
|
<Label className="text-base font-semibold cursor-pointer">
|
||||||
Active Visibility
|
Active Visibility
|
||||||
@@ -733,7 +762,8 @@ export default function HealthPackagePage() {
|
|||||||
value={pkgForm.categoryId?.toString()}
|
value={pkgForm.categoryId?.toString()}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setPkgForm({ ...pkgForm, categoryId: Number(v) })
|
setPkgForm({ ...pkgForm, categoryId: Number(v) })
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<SelectTrigger className="text-base">
|
<SelectTrigger className="text-base">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -814,15 +844,15 @@ export default function HealthPackagePage() {
|
|||||||
{inclusionsList.map((inc) => (
|
{inclusionsList.map((inc) => (
|
||||||
<div
|
<div
|
||||||
key={inc.id}
|
key={inc.id}
|
||||||
className="p-4 border rounded-md bg-muted/10 relative">
|
className="p-4 border rounded-md bg-muted/10 relative"
|
||||||
|
>
|
||||||
{/* Remove Button */}
|
{/* Remove Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-2 right-2 h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50"
|
className="absolute top-2 right-2 h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||||
onClick={() =>
|
onClick={() => handleRemoveInclusionField(inc.id)}
|
||||||
handleRemoveInclusionField(inc.id)
|
>
|
||||||
}>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -873,7 +903,8 @@ export default function HealthPackagePage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full mt-2 border-dashed border-2"
|
className="w-full mt-2 border-dashed border-2"
|
||||||
onClick={handleAddInclusionField}>
|
onClick={handleAddInclusionField}
|
||||||
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add New Category Group
|
Add New Category Group
|
||||||
</Button>
|
</Button>
|
||||||
@@ -888,7 +919,8 @@ export default function HealthPackagePage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPackageModal(false)}
|
onClick={() => setPackageModal(false)}
|
||||||
className="text-base">
|
className="text-base"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={savePackage} className="px-10 text-base">
|
<Button onClick={savePackage} className="px-10 text-base">
|
||||||
@@ -1008,7 +1040,8 @@ export default function HealthPackagePage() {
|
|||||||
tests.map((item, i) => (
|
tests.map((item, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-sm border p-3 rounded bg-background shadow-sm">
|
className="text-sm border p-3 rounded bg-background shadow-sm"
|
||||||
|
>
|
||||||
✓ {item}
|
✓ {item}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -1022,7 +1055,8 @@ export default function HealthPackagePage() {
|
|||||||
selectedPackage.inclusions.map((item, i) => (
|
selectedPackage.inclusions.map((item, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-sm border p-3 rounded bg-background shadow-sm">
|
className="text-sm border p-3 rounded bg-background shadow-sm"
|
||||||
|
>
|
||||||
✓ {item}
|
✓ {item}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user